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 @@
+
+
+
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 @@
+
+
+