Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4fdf3f7

Browse filesBrowse files
committed
feature #59004 [AssetMapper] Detect import with a sequence parser (smnandre)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [AssetMapper] Detect import with a sequence parser | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #58928 and #58944 (maybe) | License | MIT Third attempt after #54134 and #58999.. this is the good one 👍 I initially started this PR to learn / try things, but it really behaves well.. and _may_ stop (or help stopping) what seems to be a never-ending suite of special cases. Open to any feedback Commits ------- 720c387 [AssetMapper] Detect import with a sequence parser
2 parents ec4b5c7 + 720c387 commit 4fdf3f7
Copy full SHA for 4fdf3f7

File tree

4 files changed

+427
-43
lines changed
Filter options

4 files changed

+427
-43
lines changed

‎src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
+6-34Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\AssetMapper\AssetMapperInterface;
16+
use Symfony\Component\AssetMapper\Compiler\Parser\JavascriptSequenceParser;
1617
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
1718
use Symfony\Component\AssetMapper\Exception\RuntimeException;
1819
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
@@ -61,15 +62,13 @@ public function __construct(
6162

6263
public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
6364
{
64-
return preg_replace_callback(self::IMPORT_PATTERN, function ($matches) use ($asset, $assetMapper, $content) {
65-
$fullImportString = $matches[0][0];
65+
$jsParser = new JavascriptSequenceParser($content);
6666

67-
// Ignore matches that did not capture import statements
68-
if (!isset($matches[1][0])) {
69-
return $fullImportString;
70-
}
67+
return preg_replace_callback(self::IMPORT_PATTERN, function ($matches) use ($asset, $assetMapper, $jsParser) {
68+
$fullImportString = $matches[0][0];
7169

72-
if ($this->isCommentedOut($matches[0][1], $content)) {
70+
$jsParser->parseUntil($matches[0][1]);
71+
if (!$jsParser->isExecutable()) {
7372
return $fullImportString;
7473
}
7574

@@ -146,33 +145,6 @@ private function handleMissingImport(string $message, ?\Throwable $e = null): vo
146145
};
147146
}
148147

149-
/**
150-
* Simple check for the most common types of comments.
151-
*
152-
* This is not a full parser, but should be good enough for most cases.
153-
*/
154-
private function isCommentedOut(mixed $offsetStart, string $fullContent): bool
155-
{
156-
$lineStart = strrpos($fullContent, "\n", $offsetStart - \strlen($fullContent));
157-
$lineContentBeforeImport = substr($fullContent, $lineStart, $offsetStart - $lineStart);
158-
$firstTwoChars = substr(ltrim($lineContentBeforeImport), 0, 2);
159-
if ('//' === $firstTwoChars) {
160-
return true;
161-
}
162-
163-
if ('/*' === $firstTwoChars) {
164-
$commentEnd = strpos($fullContent, '*/', $lineStart);
165-
// if we can't find the end comment, be cautious: assume this is not a comment
166-
if (false === $commentEnd) {
167-
return false;
168-
}
169-
170-
return $offsetStart < $commentEnd;
171-
}
172-
173-
return false;
174-
}
175-
176148
private function findAssetForBareImport(string $importedModule, AssetMapperInterface $assetMapper): ?MappedAsset
177149
{
178150
if (!$importMapEntry = $this->importMapConfigReader->findRootImportMapEntry($importedModule)) {
+187Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\Compiler\Parser;
13+
14+
/**
15+
* Parses JavaScript content to identify sequences of strings, comments, etc.
16+
*
17+
* @author Simon André <smn.andre@gmail.com>
18+
*
19+
* @internal
20+
*/
21+
final class JavascriptSequenceParser
22+
{
23+
private const STATE_DEFAULT = 0;
24+
private const STATE_COMMENT = 1;
25+
private const STATE_STRING = 2;
26+
27+
private int $cursor = 0;
28+
29+
private int $contentEnd;
30+
31+
private string $pattern;
32+
33+
private int $currentSequenceType = self::STATE_DEFAULT;
34+
35+
private ?int $currentSequenceEnd = null;
36+
37+
private const COMMENT_SEPARATORS = [
38+
'/*', // Multi-line comment
39+
'//', // Single-line comment
40+
'"', // Double quote
41+
'\'', // Single quote
42+
'`', // Backtick
43+
];
44+
45+
public function __construct(
46+
private readonly string $content,
47+
) {
48+
$this->contentEnd = \strlen($content);
49+
50+
$this->pattern ??= '/'.implode('|', array_map(
51+
fn (string $ch): string => preg_quote($ch, '/'),
52+
self::COMMENT_SEPARATORS
53+
)).'/';
54+
}
55+
56+
public function isString(): bool
57+
{
58+
return self::STATE_STRING === $this->currentSequenceType;
59+
}
60+
61+
public function isExecutable(): bool
62+
{
63+
return self::STATE_DEFAULT === $this->currentSequenceType;
64+
}
65+
66+
public function isComment(): bool
67+
{
68+
return self::STATE_COMMENT === $this->currentSequenceType;
69+
}
70+
71+
public function parseUntil(int $position): void
72+
{
73+
if ($position > $this->contentEnd) {
74+
throw new \RuntimeException('Cannot parse beyond the end of the content.');
75+
}
76+
if ($position < $this->cursor) {
77+
throw new \RuntimeException('Cannot parse backwards.');
78+
}
79+
80+
while ($this->cursor <= $position) {
81+
// Current CodeSequence ?
82+
if (null !== $this->currentSequenceEnd) {
83+
if ($this->currentSequenceEnd > $position) {
84+
$this->cursor = $position;
85+
86+
return;
87+
}
88+
89+
$this->cursor = $this->currentSequenceEnd;
90+
$this->setSequence(self::STATE_DEFAULT, null);
91+
}
92+
93+
preg_match($this->pattern, $this->content, $matches, \PREG_OFFSET_CAPTURE, $this->cursor);
94+
if (!$matches) {
95+
$this->endsWithSequence(self::STATE_DEFAULT, $position);
96+
97+
return;
98+
}
99+
100+
$matchPos = (int) $matches[0][1];
101+
$matchChar = $matches[0][0];
102+
103+
if ($matchPos > $position) {
104+
$this->setSequence(self::STATE_DEFAULT, $matchPos - 1);
105+
$this->cursor = $position;
106+
107+
return;
108+
}
109+
110+
// Multi-line comment
111+
if ('/*' === $matchChar) {
112+
if (false === $endPos = strpos($this->content, '*/', $matchPos + 2)) {
113+
$this->endsWithSequence(self::STATE_COMMENT, $position);
114+
115+
return;
116+
}
117+
118+
$this->cursor = min($endPos + 2, $position);
119+
$this->setSequence(self::STATE_COMMENT, $endPos + 2);
120+
continue;
121+
}
122+
123+
// Single-line comment
124+
if ('//' === $matchChar) {
125+
if (false === $endPos = strpos($this->content, "\n", $matchPos + 2)) {
126+
$this->endsWithSequence(self::STATE_COMMENT, $position);
127+
128+
return;
129+
}
130+
131+
$this->cursor = min($endPos + 1, $position);
132+
$this->setSequence(self::STATE_COMMENT, $endPos + 1);
133+
continue;
134+
}
135+
136+
// Single-line string
137+
if ('"' === $matchChar || "'" === $matchChar) {
138+
if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) {
139+
$this->endsWithSequence(self::STATE_STRING, $position);
140+
141+
return;
142+
}
143+
while (false !== $endPos && '\\' == $this->content[$endPos - 1]) {
144+
$endPos = strpos($this->content, $matchChar, $endPos + 1);
145+
}
146+
147+
$this->cursor = min($endPos + 1, $position);
148+
$this->setSequence(self::STATE_STRING, $endPos + 1);
149+
continue;
150+
}
151+
152+
// Multi-line string
153+
if ('`' === $matchChar) {
154+
if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) {
155+
$this->endsWithSequence(self::STATE_STRING, $position);
156+
157+
return;
158+
}
159+
while (false !== $endPos && '\\' == $this->content[$endPos - 1]) {
160+
$endPos = strpos($this->content, $matchChar, $endPos + 1);
161+
}
162+
163+
$this->cursor = min($endPos + 1, $position);
164+
$this->setSequence(self::STATE_STRING, $endPos + 1);
165+
}
166+
}
167+
}
168+
169+
/**
170+
* @param int<self::STATE_*> $type
171+
*/
172+
private function endsWithSequence(int $type, int $cursor): void
173+
{
174+
$this->cursor = $cursor;
175+
$this->currentSequenceType = $type;
176+
$this->currentSequenceEnd = $this->contentEnd;
177+
}
178+
179+
/**
180+
* @param int<self::STATE_*> $type
181+
*/
182+
private function setSequence(int $type, ?int $end = null): void
183+
{
184+
$this->currentSequenceType = $type;
185+
$this->currentSequenceEnd = $end;
186+
}
187+
}

‎src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
-9Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,6 @@ public static function provideCompileTests(): iterable
290290
'expectedJavaScriptImports' => [],
291291
];
292292

293-
yield 'multi_line_comment_with_no_end_parsed_for_safety' => [
294-
'input' => <<<EOF
295-
const fun;
296-
/* comment import("./other.js");
297-
EOF
298-
,
299-
'expectedJavaScriptImports' => ['/assets/other.js' => ['lazy' => true, 'asset' => 'other.js', 'add' => true]],
300-
];
301-
302293
yield 'multi_line_comment_with_no_end_found_eventually_ignored' => [
303294
'input' => <<<EOF
304295
const fun;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.