Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[AssetMapper] Detect import with a sequence parser
  • Loading branch information
smnandre authored and fabpot committed Jan 25, 2025
commit 720c38746083b0578ca8f7deee96f30378edb3d4
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Psr\Log\LoggerInterface;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Compiler\Parser\JavascriptSequenceParser;
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
Expand Down Expand Up @@ -61,15 +62,13 @@ public function __construct(

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

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

if ($this->isCommentedOut($matches[0][1], $content)) {
$jsParser->parseUntil($matches[0][1]);
if (!$jsParser->isExecutable()) {
return $fullImportString;
}

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

/**
* Simple check for the most common types of comments.
*
* This is not a full parser, but should be good enough for most cases.
*/
private function isCommentedOut(mixed $offsetStart, string $fullContent): bool
{
$lineStart = strrpos($fullContent, "\n", $offsetStart - \strlen($fullContent));
$lineContentBeforeImport = substr($fullContent, $lineStart, $offsetStart - $lineStart);
$firstTwoChars = substr(ltrim($lineContentBeforeImport), 0, 2);
if ('//' === $firstTwoChars) {
return true;
}

if ('/*' === $firstTwoChars) {
$commentEnd = strpos($fullContent, '*/', $lineStart);
// if we can't find the end comment, be cautious: assume this is not a comment
if (false === $commentEnd) {
return false;
}

return $offsetStart < $commentEnd;
}

return false;
}

private function findAssetForBareImport(string $importedModule, AssetMapperInterface $assetMapper): ?MappedAsset
{
if (!$importMapEntry = $this->importMapConfigReader->findRootImportMapEntry($importedModule)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\Compiler\Parser;

/**
* Parses JavaScript content to identify sequences of strings, comments, etc.
*
* @author Simon AndrΓ© <[email protected]>
*
* @internal
*/
final class JavascriptSequenceParser
{
private const STATE_DEFAULT = 0;
private const STATE_COMMENT = 1;
private const STATE_STRING = 2;

private int $cursor = 0;

private int $contentEnd;

private string $pattern;

private int $currentSequenceType = self::STATE_DEFAULT;

private ?int $currentSequenceEnd = null;

private const COMMENT_SEPARATORS = [
'/*', // Multi-line comment
'//', // Single-line comment
'"', // Double quote
'\'', // Single quote
'`', // Backtick
];

public function __construct(
private readonly string $content,
) {
$this->contentEnd = \strlen($content);

$this->pattern ??= '/'.implode('|', array_map(
fn (string $ch): string => preg_quote($ch, '/'),
self::COMMENT_SEPARATORS
)).'/';
}

public function isString(): bool
{
return self::STATE_STRING === $this->currentSequenceType;
}

public function isExecutable(): bool
{
return self::STATE_DEFAULT === $this->currentSequenceType;
}

public function isComment(): bool
{
return self::STATE_COMMENT === $this->currentSequenceType;
}

public function parseUntil(int $position): void
{
if ($position > $this->contentEnd) {
throw new \RuntimeException('Cannot parse beyond the end of the content.');
}
if ($position < $this->cursor) {
throw new \RuntimeException('Cannot parse backwards.');
}

while ($this->cursor <= $position) {
// Current CodeSequence ?
if (null !== $this->currentSequenceEnd) {
if ($this->currentSequenceEnd > $position) {
$this->cursor = $position;

return;
}

$this->cursor = $this->currentSequenceEnd;
$this->setSequence(self::STATE_DEFAULT, null);
}

preg_match($this->pattern, $this->content, $matches, \PREG_OFFSET_CAPTURE, $this->cursor);
if (!$matches) {
$this->endsWithSequence(self::STATE_DEFAULT, $position);

return;
}

$matchPos = (int) $matches[0][1];
$matchChar = $matches[0][0];

if ($matchPos > $position) {
$this->setSequence(self::STATE_DEFAULT, $matchPos - 1);
$this->cursor = $position;

return;
}

// Multi-line comment
if ('/*' === $matchChar) {
if (false === $endPos = strpos($this->content, '*/', $matchPos + 2)) {
$this->endsWithSequence(self::STATE_COMMENT, $position);

return;
}

$this->cursor = min($endPos + 2, $position);
$this->setSequence(self::STATE_COMMENT, $endPos + 2);
continue;
}

// Single-line comment
if ('//' === $matchChar) {
if (false === $endPos = strpos($this->content, "\n", $matchPos + 2)) {
$this->endsWithSequence(self::STATE_COMMENT, $position);

return;
}

$this->cursor = min($endPos + 1, $position);
$this->setSequence(self::STATE_COMMENT, $endPos + 1);
continue;
}

// Single-line string
if ('"' === $matchChar || "'" === $matchChar) {
if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) {
$this->endsWithSequence(self::STATE_STRING, $position);

return;
}
while (false !== $endPos && '\\' == $this->content[$endPos - 1]) {
$endPos = strpos($this->content, $matchChar, $endPos + 1);
}

$this->cursor = min($endPos + 1, $position);
$this->setSequence(self::STATE_STRING, $endPos + 1);
continue;
}

// Multi-line string
if ('`' === $matchChar) {
if (false === $endPos = strpos($this->content, $matchChar, $matchPos + 1)) {
$this->endsWithSequence(self::STATE_STRING, $position);

return;
}
while (false !== $endPos && '\\' == $this->content[$endPos - 1]) {
$endPos = strpos($this->content, $matchChar, $endPos + 1);
}

$this->cursor = min($endPos + 1, $position);
$this->setSequence(self::STATE_STRING, $endPos + 1);
}
}
}

/**
* @param int<self::STATE_*> $type
*/
private function endsWithSequence(int $type, int $cursor): void
{
$this->cursor = $cursor;
$this->currentSequenceType = $type;
$this->currentSequenceEnd = $this->contentEnd;
}

/**
* @param int<self::STATE_*> $type
*/
private function setSequence(int $type, ?int $end = null): void
{
$this->currentSequenceType = $type;
$this->currentSequenceEnd = $end;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,6 @@ public static function provideCompileTests(): iterable
'expectedJavaScriptImports' => [],
];

yield 'multi_line_comment_with_no_end_parsed_for_safety' => [
'input' => <<<EOF
const fun;
/* comment import("./other.js");
EOF
,
'expectedJavaScriptImports' => ['/assets/other.js' => ['lazy' => true, 'asset' => 'other.js', 'add' => true]],
];

yield 'multi_line_comment_with_no_end_found_eventually_ignored' => [
'input' => <<<EOF
const fun;
Expand Down
Loading
Loading