diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_pagetsconfig.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_pagetsconfig.xlf index 0537d176b19a..90f55841d4f7 100644 --- a/typo3/sysext/backend/Resources/Private/Language/locallang_pagetsconfig.xlf +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_pagetsconfig.xlf @@ -135,6 +135,9 @@ Brace missing in "%1$s", line number "%2$s" + + Import does not find a file in "%1$s", line number "%2$s" + Code segment diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitor.php index 7a3fcf5d3a1f..220f23d4ce86 100644 --- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitor.php +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitor.php @@ -17,9 +17,16 @@ namespace TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\AtImportInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionElseInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionIncludeTyposcriptInclude; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeTyposcriptInclude; use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine; use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine; +use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportLine; +use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportOldLine; use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\InvalidLine; use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineInterface; @@ -32,12 +39,12 @@ final class IncludeTreeSyntaxScannerVisitor implements IncludeTreeVisitorInterface { /** - * @var list + * @var list */ private array $errors = []; /** - * @return list + * @return list */ public function getErrors(): array { @@ -49,6 +56,27 @@ public function visitBeforeChildren(IncludeInterface $include, int $currentDepth } public function visit(IncludeInterface $include, int $currentDepth): void + { + $this->brokenLinesAndBraces($include); + $this->emptyImports($include); + + // Add the line number of the first token of the line object to the error array. + // Not strictly needed, but more convenient in Fluid template to render. + foreach ($this->errors as &$error) { + /** @var LineInterface $line */ + $line = $error['line']; + $error['lineNumber'] = $line->getTokenStream()->reset()->peekNext()->getLine(); + } + + // Sort array by line number to list them top->bottom in view. + usort($this->errors, fn ($a, $b) => $a['lineNumber'] <=> $b['lineNumber']); + } + + /** + * Scan for invalid lines ("foo.bar <" is invalid since there must be something after "<"), + * and scan for "too many" and "not enough" "}" braces. + */ + private function brokenLinesAndBraces(IncludeInterface $include): void { if ($include->isSplit()) { // If this node is split, don't check for syntax errors, this is @@ -92,13 +120,67 @@ public function visit(IncludeInterface $include, int $currentDepth): void 'line' => $lastLine, ]; } + } - // Add the line number of the first token of the line object to the error array. - // Not strictly needed, but more convenient in Fluid template to render. - foreach ($this->errors as &$error) { - /** @var LineInterface $line */ - $line = $error['line']; - $error['lineNumber'] = $line->getTokenStream()->reset()->peekNext()->getLine(); + /** + * Look for @import and INCLUDE_TYPOSCRIPT that don't find to-include file(s). + * + * @todo: This code is far more complex than it could be. See #102102 and #102103 for + * changes we should apply to the include tree structure to simplify this. + */ + private function emptyImports(IncludeInterface $include): void + { + if (!$include->isSplit()) { + // Nodes containing @import are always split + return; + } + $lineStream = $include->getLineStream(); + if (!$lineStream) { + // A node that is split should never have an empty line stream, + // this may be obsolete, but does not hurt much. + return; + } + // Find @import lines in this include, index by + // combination of line number and column position. + $allImportLines = []; + foreach ($lineStream->getNextLine() as $line) { + if ($line instanceof ImportLine || $line instanceof ImportOldLine) { + $valueToken = $line->getValueToken(); + $allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()] = $line; + } + } + // Now iterate children to exclude valid allImportLines, those that included something. + foreach ($include->getNextChild() as $child) { + if ($child instanceof AtImportInclude || $child instanceof IncludeTyposcriptInclude) { + /** @var ImportLine|ImportOldLine $originalLine */ + $originalLine = $child->getOriginalLine(); + $valueToken = $originalLine->getValueToken(); + unset($allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()]); + } + // Condition includes don't have the "body" lines itself (or a "body" sub node). This may change, + // but until then we'll have to scan the parent node and loop condition includes here to find out + // which of them resolved to child nodes. + if ($child instanceof ConditionInclude + || $child instanceof ConditionElseInclude + || $child instanceof ConditionIncludeTyposcriptInclude + ) { + foreach ($child->getNextChild() as $conditionChild) { + if ($conditionChild instanceof AtImportInclude || $conditionChild instanceof IncludeTyposcriptInclude) { + /** @var ImportLine|ImportOldLine $originalLine */ + $originalLine = $conditionChild->getOriginalLine(); + $valueToken = $originalLine->getValueToken(); + unset($allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()]); + } + } + } + } + // Everything left are invalid includes + foreach ($allImportLines as $importLine) { + $this->errors[] = [ + 'type' => 'import.empty', + 'include' => $include, + 'line' => $importLine, + ]; } } } diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-TypoScriptSyntaxImprovements.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-TypoScriptSyntaxImprovements.rst index cff17e30e944..80499e3e971c 100644 --- a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-TypoScriptSyntaxImprovements.rst +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-TypoScriptSyntaxImprovements.rst @@ -70,9 +70,9 @@ syntax, and integrators are encouraged to fully switch to :typoscript:`@import`. .. code-block:: typoscript [frontend.user.isLoggedIn] - @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript' [ELSE] - @import 'EXT:my_extension/Configuration/TypoScript/NotLoggedInUser.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/NotLoggedInUser.typoscript' [END] Scope restriction to file / snipped level @@ -96,9 +96,9 @@ two conditions follow directly in one snippet: .. code-block:: typoscript [frontend.user.isLoggedIn] - @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript' [applicationContext == "Development"] - @import 'EXT:my_extension/Configuration/TypoScript/Development.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/Development.typoscript' [END] This always worked and did not change with the new parser: Opening a new condition @@ -113,12 +113,12 @@ included if a user is logged in *and* the application is in development context. .. code-block:: typoscript [frontend.user.isLoggedIn] - @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUser.typoscript' [END] # File LoggedInUser.typoscript: [applicationContext == "Development"] - @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUserDevelopment.typoscript + @import 'EXT:my_extension/Configuration/TypoScript/LoggedInUserDevelopment.typoscript' [END] Irrelevant order of + +[frontend.user.isLoggedIn] + @import './Imports/validImportC.typoscript' + @import './invalidImport.typoscript' + @import './Imports/validImportD*.typoscript' + + + this.is.invalid < +[global] +this.is.invalid < diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitorTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitorTest.php index ec3a969c69c3..8c42815e8f70 100644 --- a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitorTest.php +++ b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/Visitor/IncludeTreeSyntaxScannerVisitorTest.php @@ -19,13 +19,26 @@ use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\FileInclude; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSyntaxScannerVisitor; use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; final class IncludeTreeSyntaxScannerVisitorTest extends FunctionalTestCase { - protected bool $initializeDatabase = false; + /** + * Helper method to remove line. This is more convenient + * to compare, we simply rely on lineNumber. + */ + private function removeLineFromErrors(array $errors): array + { + foreach ($errors as &$error) { + unset($error['line']); + } + return $errors; + } public static function visitDataProvider(): iterable { @@ -125,14 +138,26 @@ public function visit(IncludeInterface $node, array $expectedErrors): void } /** - * Helper method to remove line. This is more convenient - * to compare, we simply rely on lineNumber. + * @test */ - private function removeLineFromErrors(array $errors): array + public function visitFindsEmptyImports() { - foreach ($errors as &$error) { - unset($error['line']); - } - return $errors; + $this->importCSVDataSet(__DIR__ . '/../Fixtures/IncludeTreeSyntaxScannerVisitor/RootTemplate.csv'); + $rootline = [ + [ + 'uid' => 1, + 'pid' => 0, + 'is_siteroot' => 0, + ], + ]; + $sysTemplateRepository = $this->get(SysTemplateRepository::class); + $subject = $this->get(SysTemplateTreeBuilder::class); + $includeTree = $subject->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline), new LosslessTokenizer()); + $traverser = new IncludeTreeTraverser(); + $visitor = new IncludeTreeSyntaxScannerVisitor(); + $traverser->traverse($includeTree, [$visitor]); + $erroneousLineNumbers = array_column($visitor->getErrors(), 'lineNumber'); + $expectedLineNumbers = [0, 2, 4, 6, 9, 12, 13, 15]; + self::assertSame($expectedLineNumbers, $erroneousLineNumbers); } } diff --git a/typo3/sysext/tstemplate/Resources/Private/Language/locallang_analyzer.xlf b/typo3/sysext/tstemplate/Resources/Private/Language/locallang_analyzer.xlf index 0c0123264d69..0e09da033486 100644 --- a/typo3/sysext/tstemplate/Resources/Private/Language/locallang_analyzer.xlf +++ b/typo3/sysext/tstemplate/Resources/Private/Language/locallang_analyzer.xlf @@ -60,6 +60,9 @@ Brace missing in "%1$s", line number "%2$s" + + Import does not find a file in "%1$s", line number "%2$s" + Show code