Skip to content

Commit 70777e5

Browse files
committed
Use YAML scalar facts for environment expressions
1 parent d4958ba commit 70777e5

4 files changed

Lines changed: 102 additions & 44 deletions

File tree

src/Feature/Environment/EnvironmentExtractor.php

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use Symfony\Lsp\Parser\Php\PhpCommentParserInterface;
77
use Symfony\Lsp\Parser\Twig\TwigCommentParser;
88
use Symfony\Lsp\Parser\Xml\XmlCommentParser;
9-
use Symfony\Lsp\Parser\Yaml\YamlCommentParser;
9+
use Symfony\Lsp\Parser\Yaml\YamlDocumentParser;
1010
use Symfony\Lsp\Project\UriToPathConverter;
1111

1212
final class EnvironmentExtractor
@@ -16,7 +16,7 @@ public function __construct(
1616
private readonly UriToPathConverter $uriToPathConverter,
1717
private readonly TwigCommentParser $commentParser,
1818
private readonly PhpCommentParserInterface $phpComments,
19-
private readonly YamlCommentParser $yamlComments,
19+
private readonly YamlDocumentParser $yamlParser,
2020
private readonly XmlCommentParser $xmlComments,
2121
) {
2222
}
@@ -42,30 +42,44 @@ public function extract(string $uri, string $languageId, string $text): Environm
4242
$references[] = new EnvironmentReference($name, $uri, $this->converter->toRange($text, $offset, \strlen($name)), []);
4343
}
4444
}
45-
if (\in_array($languageId, ['php', 'twig', 'yaml', 'xml'], true)) {
45+
if ('yaml' === $languageId) {
46+
foreach ($this->yamlParser->parseDocument($text)->scalars as $scalar) {
47+
$contentOffset = $scalar->contentStartByte - $scalar->startByte;
48+
$scalarText = substr($scalar->raw, $contentOffset, $scalar->contentEndByte - $scalar->contentStartByte);
49+
array_push($references, ...$this->references($uri, $text, $scalarText, $scalar->contentStartByte));
50+
}
51+
} elseif (\in_array($languageId, ['php', 'twig', 'xml'], true)) {
4652
$referenceText = match ($languageId) {
4753
'twig' => $this->commentParser->mask($text),
4854
'php' => $this->phpComments->mask($text),
49-
'yaml' => $this->yamlComments->mask($text),
5055
'xml' => $this->xmlComments->mask($text),
5156
};
52-
preg_match_all('/%env\(([^)%]+)\)%/', $referenceText, $matches, \PREG_OFFSET_CAPTURE);
53-
foreach ($matches[1] as [$expression, $offset]) {
54-
$parts = explode(':', $expression);
55-
$name = array_pop($parts);
56-
if (1 !== preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) {
57-
continue;
58-
}
59-
$nameOffset = $offset + \strlen($expression) - \strlen($name);
60-
$references[] = new EnvironmentReference(
61-
$name,
62-
$uri,
63-
$this->converter->toRange($text, $nameOffset, \strlen($name)),
64-
$parts,
65-
);
66-
}
57+
array_push($references, ...$this->references($uri, $text, $referenceText));
6758
}
6859

6960
return new EnvironmentSourceFacts($uri, $declarations, $references);
7061
}
62+
63+
/** @return list<EnvironmentReference> */
64+
private function references(string $uri, string $text, string $referenceText, int $baseOffset = 0): array
65+
{
66+
preg_match_all('/%env\(([^)%]+)\)%/', $referenceText, $matches, \PREG_OFFSET_CAPTURE);
67+
$references = [];
68+
foreach ($matches[1] as [$expression, $offset]) {
69+
$parts = explode(':', $expression);
70+
$name = array_pop($parts);
71+
if (1 !== preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) {
72+
continue;
73+
}
74+
$nameOffset = $baseOffset + $offset + \strlen($expression) - \strlen($name);
75+
$references[] = new EnvironmentReference(
76+
$name,
77+
$uri,
78+
$this->converter->toRange($text, $nameOffset, \strlen($name)),
79+
$parts,
80+
);
81+
}
82+
83+
return $references;
84+
}
7185
}

src/Feature/Environment/EnvironmentProvider.php

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use Symfony\Lsp\Parser\Php\PhpCommentParserInterface;
1515
use Symfony\Lsp\Parser\Twig\TwigCommentParser;
1616
use Symfony\Lsp\Parser\Xml\XmlCommentParser;
17-
use Symfony\Lsp\Parser\Yaml\YamlCommentParser;
17+
use Symfony\Lsp\Parser\Yaml\YamlDocumentParser;
1818
use Symfony\Lsp\Project\Project;
1919
use Symfony\Lsp\Protocol\LspProtocolMapper;
2020

@@ -31,7 +31,7 @@ public function __construct(
3131
private readonly EnvironmentExtractor $extractor,
3232
private readonly TwigCommentParser $commentParser,
3333
private readonly PhpCommentParserInterface $phpComments,
34-
private readonly YamlCommentParser $yamlComments,
34+
private readonly YamlDocumentParser $yamlParser,
3535
private readonly XmlCommentParser $xmlComments,
3636
) {
3737
}
@@ -43,8 +43,8 @@ public function complete(array $params): ?array
4343
return null;
4444
}
4545
$cursor = $this->converter->toByteOffset($request->document->text, $request->position);
46-
$text = $this->commentFreeText($request->document->languageId, $request->document->text);
47-
if (!preg_match('/%env\(([^)]*)$/', substr($text, 0, $cursor), $match, \PREG_OFFSET_CAPTURE)) {
46+
$textBeforeCursor = $this->textBeforeCursor($request->document->languageId, $request->document->text, $cursor);
47+
if (null === $textBeforeCursor || !preg_match('/%env\(([^)]*)$/', $textBeforeCursor, $match, \PREG_OFFSET_CAPTURE)) {
4848
return null;
4949
}
5050
$expression = $match[1][0];
@@ -153,9 +153,7 @@ public function diagnostics(array $params): ?array
153153
$previousProcessor = $processor;
154154
}
155155
}
156-
$text = $this->commentFreeText($request->document->languageId, $request->document->text);
157-
preg_match_all('/%env\([^\)\r\n]*%/', $text, $malformed, \PREG_OFFSET_CAPTURE);
158-
foreach ($malformed[0] as [$expression, $offset]) {
156+
foreach ($this->malformedExpressions($request->document->languageId, $request->document->text) as [$expression, $offset]) {
159157
$range = new Range($this->converter->toPosition($request->document->text, $offset), $this->converter->toPosition($request->document->text, $offset + \strlen($expression)));
160158
$diagnostics[] = $this->protocol->diagnostic($range, 1, 'env.malformed_chain', 'Malformed environment expression; expected ")%".');
161159
}
@@ -190,12 +188,52 @@ private function resolve(array $params): ?array
190188
return null;
191189
}
192190

191+
private function textBeforeCursor(string $languageId, string $text, int $cursor): ?string
192+
{
193+
if ('yaml' === $languageId) {
194+
foreach ($this->yamlParser->parseDocument($text)->scalars as $scalar) {
195+
if ($cursor < $scalar->contentStartByte || $cursor > $scalar->contentEndByte) {
196+
continue;
197+
}
198+
199+
$contentOffset = $scalar->contentStartByte - $scalar->startByte;
200+
201+
return substr($scalar->raw, $contentOffset, $cursor - $scalar->contentStartByte);
202+
}
203+
204+
return null;
205+
}
206+
207+
return substr($this->commentFreeText($languageId, $text), 0, $cursor);
208+
}
209+
210+
/** @return iterable<array{string, int}> */
211+
private function malformedExpressions(string $languageId, string $text): iterable
212+
{
213+
if ('yaml' === $languageId) {
214+
foreach ($this->yamlParser->parseDocument($text)->scalars as $scalar) {
215+
$contentOffset = $scalar->contentStartByte - $scalar->startByte;
216+
$scalarText = substr($scalar->raw, $contentOffset, $scalar->contentEndByte - $scalar->contentStartByte);
217+
preg_match_all('/%env\([^\)\r\n]*%/', $scalarText, $malformed, \PREG_OFFSET_CAPTURE);
218+
foreach ($malformed[0] as [$expression, $offset]) {
219+
yield [$expression, $scalar->contentStartByte + $offset];
220+
}
221+
}
222+
223+
return;
224+
}
225+
226+
preg_match_all('/%env\([^\)\r\n]*%/', $this->commentFreeText($languageId, $text), $malformed, \PREG_OFFSET_CAPTURE);
227+
foreach ($malformed[0] as $match) {
228+
yield $match;
229+
}
230+
}
231+
193232
private function commentFreeText(string $languageId, string $text): string
194233
{
195234
return match ($languageId) {
196235
'twig' => $this->commentParser->mask($text),
197236
'php' => $this->phpComments->mask($text),
198-
'yaml' => $this->yamlComments->mask($text),
199237
'xml' => $this->xmlComments->mask($text),
200238
default => $text,
201239
};

tests/Feature/Environment/EnvironmentProviderTest.php

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
use Symfony\Lsp\Feature\Environment\EnvironmentIndexRegistry;
1414
use Symfony\Lsp\Feature\Environment\EnvironmentProvider;
1515
use Symfony\Lsp\Parser\Php\PhpCommentParser;
16+
use Symfony\Lsp\Parser\TreeSitter\NativeTreeSitterParser;
17+
use Symfony\Lsp\Parser\TreeSitter\TreeSitterResultDecoder;
1618
use Symfony\Lsp\Parser\Twig\TwigCommentParser;
1719
use Symfony\Lsp\Parser\Xml\XmlCommentParser;
18-
use Symfony\Lsp\Parser\Yaml\YamlCommentParser;
20+
use Symfony\Lsp\Parser\Yaml\YamlDocumentParser;
1921
use Symfony\Lsp\Project\Project;
2022
use Symfony\Lsp\Project\ProjectRegistry;
2123
use Symfony\Lsp\Project\UriToPathConverter;
@@ -25,7 +27,7 @@ final class EnvironmentProviderTest extends TestCase
2527
{
2628
public function testIndexesNamesAndReferencesWithoutValues(): void
2729
{
28-
$extractor = new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), new YamlCommentParser(), new XmlCommentParser());
30+
$extractor = new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), $this->yamlParser(), new XmlCommentParser());
2931
$facts = $extractor->extract('file:///workspace/.env', 'dotenv', "APP_SECRET=CANARY_SECRET_VALUE\nAPP_URL=https://example.com\nEMPTY=\nCHILD=\${APP_URL:-\${FALLBACK_URL}}/\$EMPTY\nPARTIAL=\${UNFINISHED\nESCAPED=\\\$IGNORED\n");
3032

3133
self::assertSame(['APP_SECRET', 'APP_URL', 'EMPTY', 'CHILD', 'PARTIAL', 'ESCAPED'], array_map(static fn ($item): string => $item->name, $facts->declarations));
@@ -48,15 +50,15 @@ public function testSupportsEnvironmentExpressionsInYamlScalarContexts(string $t
4850
$converter = new PositionConverter();
4951
$twigComments = new TwigCommentParser();
5052
$phpComments = new PhpCommentParser();
51-
$yamlComments = new YamlCommentParser();
53+
$yamlParser = $this->yamlParser();
5254
$xmlComments = new XmlCommentParser();
53-
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $twigComments, $phpComments, $yamlComments, $xmlComments);
55+
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $twigComments, $phpComments, $yamlParser, $xmlComments);
5456
$indexes = new EnvironmentIndexRegistry();
5557
$indexes->forProject($project)->replaceSources(
5658
$extractor->extract('file:///workspace/.env', 'dotenv', "PARTIAL_ENV=value\n"),
5759
$extractor->extract($uri, 'yaml', $text),
5860
);
59-
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $twigComments, $phpComments, $yamlComments, $xmlComments);
61+
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $twigComments, $phpComments, $yamlParser, $xmlComments);
6062

6163
$facts = $extractor->extract($uri, 'yaml', $text);
6264
self::assertSame(['COMPLETE_ENV'], array_map(static fn ($reference): string => $reference->name, $facts->references));
@@ -118,13 +120,13 @@ public function testCompletesHoversNavigatesAndDiagnosesProcessors(): void
118120
$converter = new PositionConverter();
119121
$commentParser = new TwigCommentParser();
120122
$phpComments = new PhpCommentParser();
121-
$yamlComments = new YamlCommentParser();
123+
$yamlParser = $this->yamlParser();
122124
$xmlComments = new XmlCommentParser();
123-
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $commentParser, $phpComments, $yamlComments, $xmlComments);
125+
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $commentParser, $phpComments, $yamlParser, $xmlComments);
124126
$indexes = new EnvironmentIndexRegistry();
125127
$indexes->forProject($project)->replaceSources($extractor->extract('file:///workspace/.env', 'dotenv', "APP_URL=CANARY_SECRET_VALUE\n"), $extractor->extract($uri, 'yaml', $text));
126128
$indexes->forProject($project)->replaceProcessors(['custom' => 'string', 'json' => 'array']);
127-
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $commentParser, $phpComments, $yamlComments, $xmlComments);
129+
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $commentParser, $phpComments, $yamlParser, $xmlComments);
128130
$position = $converter->toPosition($text, strpos($text, 'APP_UR') + \strlen('APP_UR'));
129131
$params = ['textDocument' => ['uri' => $uri], 'position' => ['line' => $position->line, 'character' => $position->character]];
130132
@@ -162,16 +164,16 @@ public function testIgnoresCommentedConfigurationAcrossCapabilities(string $lang
162164
$converter = new PositionConverter();
163165
$twigComments = new TwigCommentParser();
164166
$phpComments = new PhpCommentParser();
165-
$yamlComments = new YamlCommentParser();
167+
$yamlParser = $this->yamlParser();
166168
$xmlComments = new XmlCommentParser();
167-
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $twigComments, $phpComments, $yamlComments, $xmlComments);
169+
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $twigComments, $phpComments, $yamlParser, $xmlComments);
168170
$indexes = new EnvironmentIndexRegistry();
169171
$indexes->forProject($project)->replaceSources(
170172
$extractor->extract('file:///workspace/.env', 'dotenv', "APP_URL=value\n"),
171173
$extractor->extract($uri, $languageId, $text),
172174
);
173175
$indexes->forProject($project)->replaceProcessors(['json' => 'array']);
174-
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $twigComments, $phpComments, $yamlComments, $xmlComments);
176+
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $twigComments, $phpComments, $yamlParser, $xmlComments);
175177
176178
$commentCompletionOffset = strpos($text, 'APP_UR') + \strlen('APP_UR');
177179
self::assertNull($provider->complete($this->positionParams($converter, $uri, $text, $commentCompletionOffset)));
@@ -233,12 +235,12 @@ public function testOffersNoEnvironmentCompletionsInsidePhpComments(): void
233235
$converter = new PositionConverter();
234236
$commentParser = new TwigCommentParser();
235237
$phpComments = new PhpCommentParser();
236-
$yamlComments = new YamlCommentParser();
238+
$yamlParser = $this->yamlParser();
237239
$xmlComments = new XmlCommentParser();
238-
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $commentParser, $phpComments, $yamlComments, $xmlComments);
240+
$extractor = new EnvironmentExtractor($converter, new UriToPathConverter(), $commentParser, $phpComments, $yamlParser, $xmlComments);
239241
$indexes = new EnvironmentIndexRegistry();
240242
$indexes->forProject($project)->replaceSources($extractor->extract('file:///workspace/.env', 'dotenv', "APP_URL=value\n"));
241-
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $commentParser, $phpComments, $yamlComments, $xmlComments);
243+
$provider = new EnvironmentProvider(new DocumentContextResolver($documents, $projects), $converter, new LspProtocolMapper(), $indexes, $extractor, $commentParser, $phpComments, $yamlParser, $xmlComments);
242244
$completionOffset = strpos($text, 'APP_U') + \strlen('APP_U');
243245
$position = $converter->toPosition($text, $completionOffset);
244246
@@ -251,7 +253,7 @@ public function testOffersNoEnvironmentCompletionsInsidePhpComments(): void
251253
252254
public function testIgnoresEnvironmentReferencesInPhpComments(): void
253255
{
254-
$extractor = new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), new YamlCommentParser(), new XmlCommentParser());
256+
$extractor = new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), $this->yamlParser(), new XmlCommentParser());
255257
256258
$facts = $extractor->extract('file:///workspace/src/Kernel.php', 'php', <<<'PHP'
257259
<?php
@@ -263,6 +265,11 @@ public function testIgnoresEnvironmentReferencesInPhpComments(): void
263265
self::assertSame(['LIVE_ENV'], array_map(static fn ($reference): string => $reference->name, $facts->references));
264266
}
265267
268+
private function yamlParser(): YamlDocumentParser
269+
{
270+
return new YamlDocumentParser(new NativeTreeSitterParser(new TreeSitterResultDecoder()));
271+
}
272+
266273
/** @return array{textDocument: array{uri: string}, position: array{line: int, character: int}} */
267274
private function positionParams(PositionConverter $converter, string $uri, string $text, int $offset): array
268275
{

tests/Index/ApplicationSourceScannerTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
use Symfony\Lsp\Parser\TreeSitter\TreeSitterResultDecoder;
4343
use Symfony\Lsp\Parser\Twig\TwigCommentParser;
4444
use Symfony\Lsp\Parser\Xml\XmlCommentParser;
45-
use Symfony\Lsp\Parser\Yaml\YamlCommentParser;
4645
use Symfony\Lsp\Parser\Yaml\YamlDocumentParser;
4746
use Symfony\Lsp\Project\GitignoreMatcher;
4847
use Symfony\Lsp\Project\Project;
@@ -409,7 +408,7 @@ public function testPersistentFactsNeverContainEnvironmentValues(): void
409408
$indexes = new EnvironmentIndexRegistry();
410409
$this->scanner(new EnvironmentSourceIndexer(
411410
$indexes,
412-
new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), new YamlCommentParser(), new XmlCommentParser()),
411+
new EnvironmentExtractor(new PositionConverter(), new UriToPathConverter(), new TwigCommentParser(), new PhpCommentParser(), new YamlDocumentParser(new NativeTreeSitterParser(new TreeSitterResultDecoder())), new XmlCommentParser()),
413412
))->indexAll();
414413

415414
self::assertSame(['APP_SECRET'], $indexes->forProject($this->project)->names());

0 commit comments

Comments
 (0)