From 70167d3d96bedcbf15fcfb91e180295792332ef2 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 6 Dec 2025 15:56:01 +0000 Subject: [PATCH] Ast Merging Experimenting with merging ASTs to preserve the object IDs of the nodes which might help with far more efficient caching techniques. Sanity test for merging parser Failing test Argh It's in some kind of working state Nope Parser still not working Failing test Seems to work Update diff test for shortcut Add debug node ID Get full text Refactor test to simulate session Smoke test 1 Fix Make merging parser tolerant of failure Add experimental parser option TEMP: use static cache unconditionally Record number of cache misses --- .../WorseReflectionExtension.php | 8 + lib/TolerantAstDiff/AstDiff.php | 299 +++++++++++++++++ .../Tests/Benchmark/AstDiffBench.php | 24 ++ .../Tests/Unit/AstDiffTest.php | 307 ++++++++++++++++++ .../TolerantParser/Parser/MergingParser.php | 49 +++ .../Core/Inference/NodeContext.php | 14 + .../Core/Inference/NodeContextResolver.php | 3 + lib/WorseReflection/Core/ServiceLocator.php | 3 +- .../Parser/MergingParserTest.php | 109 +++++++ .../Bridge/TolerantParser/Parser/smoke.json | 11 + phpunit.xml.dist | 2 +- .../Core/Inference/NodeContext.twig | 1 + 12 files changed, 828 insertions(+), 2 deletions(-) create mode 100644 lib/TolerantAstDiff/AstDiff.php create mode 100644 lib/TolerantAstDiff/Tests/Benchmark/AstDiffBench.php create mode 100644 lib/TolerantAstDiff/Tests/Unit/AstDiffTest.php create mode 100644 lib/WorseReflection/Bridge/TolerantParser/Parser/MergingParser.php create mode 100644 lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/MergingParserTest.php create mode 100644 lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/smoke.json diff --git a/lib/Extension/WorseReflection/WorseReflectionExtension.php b/lib/Extension/WorseReflection/WorseReflectionExtension.php index 3103750361..d47cbcca3e 100644 --- a/lib/Extension/WorseReflection/WorseReflectionExtension.php +++ b/lib/Extension/WorseReflection/WorseReflectionExtension.php @@ -13,6 +13,7 @@ use Phpactor\Extension\OpenTelemetry\OpenTelemetryExtension; use Phpactor\Extension\WorseReflection\Telemetry\WorseTelemetry; use Phpactor\FilePathResolver\PathResolver; +use Phpactor\TolerantAstDiff\AstDiff; use Phpactor\WorseReflection\Bridge\Phpactor\MemberProvider\DocblockMemberProvider; use Phpactor\WorseReflection\Bridge\TolerantParser\Diagnostics\AssignmentToMissingPropertyProvider; use Phpactor\WorseReflection\Bridge\TolerantParser\Diagnostics\DeprecatedUsageDiagnosticProvider; @@ -26,6 +27,7 @@ use Phpactor\WorseReflection\Bridge\TolerantParser\Diagnostics\UndefinedVariableProvider; use Phpactor\WorseReflection\Bridge\TolerantParser\Diagnostics\UnresolvableNameProvider; use Phpactor\WorseReflection\Bridge\TolerantParser\Diagnostics\UnusedImportProvider; +use Phpactor\WorseReflection\Bridge\TolerantParser\Parser\MergingParser; use Phpactor\WorseReflection\Core\Cache; use Phpactor\WorseReflection\Core\CacheForDocument; use Phpactor\WorseReflection\Core\Cache\StaticCache; @@ -60,6 +62,7 @@ class WorseReflectionExtension implements Extension const PARAM_IMPORT_GLOBALS = 'language_server_code_transform.import_globals'; const PARAM_UNDEFINED_VAR_LEVENSHTEIN = 'worse_reflection.diagnostics.undefined_variable.suggestion_levenshtein_disatance'; const PARAM_ADDITIVE_STUBS = 'worse_reflection.additive_stubs'; + const PARAM_EXPERIMENTAL_PARSER = 'worse_reflection.experimental_parser'; public function configure(Resolver $schema): void { @@ -72,6 +75,7 @@ public function configure(Resolver $schema): void self::PARAM_STUB_DIR => '%application_root%/vendor/jetbrains/phpstorm-stubs', self::PARAM_ADDITIVE_STUBS => [], self::PARAM_UNDEFINED_VAR_LEVENSHTEIN => 4, + self::PARAM_EXPERIMENTAL_PARSER => false, ]); $schema->setDescriptions([ self::PARAM_ENABLE_CACHE => 'If reflection caching should be enabled', @@ -156,6 +160,10 @@ private function registerReflection(ContainerBuilder $container): void }); $container->register(self::SERVICE_PARSER, function (Container $container) { + if ($container->parameter(self::PARAM_EXPERIMENTAL_PARSER)->bool()) { + return new MergingParser(new AstDiff()); + } + return new CachedParser( $container->get(Cache::class), $container->get(CacheForDocument::class), diff --git a/lib/TolerantAstDiff/AstDiff.php b/lib/TolerantAstDiff/AstDiff.php new file mode 100644 index 0000000000..9e897ef0ef --- /dev/null +++ b/lib/TolerantAstDiff/AstDiff.php @@ -0,0 +1,299 @@ +fileSource1 = $node1->getRoot(); + $this->rightSource = $node2->getRoot(); + + $this->doMerge($node1, $node2); + } + + private function doMerge(Node $leftNode, Node $rightNode): void + { + if ($leftNode->getFullText() === $rightNode->getFullText()) { + return; + } + + if ($leftNode::class !== $rightNode::class) { + throw new RuntimeException(sprintf( + 'Can only compare nodes of the same type, got: %s vs %s', + $leftNode::class, + $rightNode::class + )); + } + + $node1ChildNames = $leftNode->getChildNames(); + $node2ChildNames = $rightNode->getChildNames(); + + $lastPosition = $leftNode->getFullStartPosition(); + foreach ($node1ChildNames as $childName) { + + $leftMember = $leftNode->$childName; + $rightMember = $rightNode->$childName; + + self::assertNullOrNodeToken($leftMember); + self::assertNullOrNodeToken($rightMember); + + // this property is a list of nodes we need to figure out which + // items to add, remove or update + if (is_array($leftMember) && is_array($rightMember)) { + $elementIndex = -1; + foreach (array_keys($leftMember) as $elementIndex) { + $leftElement = $leftMember[$elementIndex]; + $rightElement = $rightMember[$elementIndex] ?? null; + + $lastPosition = $leftElement->getFullStartPosition(); + + if ($rightElement === null) { + + // if there's no corresponding node in the second AST + // then remove it's correspondant and all subsequent + // nodes from the list + $this->removeChildFrom($leftNode, $childName, $elementIndex); + + // we can break early noow + break; + } + + if ($leftElement instanceof Token) { + // if we have a single token, then just replace it + // with the corresponding token/node/null + + /** @phpstan-ignore-next-line */ + $leftNode->$childName[$elementIndex] = $rightElement; + $replacement = $rightElement->getFullText( + $this->rightSource->getFileContents() + ); + $this->applyEdit(TextEdit::create( + $leftElement->getFullStartPosition(), + $leftElement->getFullWidth(), + $replacement, + )); + + continue; + } + + // if the class type is different then it's + // replace the whole subtree. + if ($leftElement::class !== $rightElement::class) { + /** @phpstan-ignore-next-line */ + $leftNode->$childName[$elementIndex] = $rightElement; + $this->applyEdit(TextEdit::create( + $leftElement->getFullStartPosition(), + $leftElement->getFullWidth(), + $rightElement->getFullText($this->rightSource->getFileContents()), + )); + continue; + } + + if ($rightElement instanceof Node) { + // recurse on the listed node: + $this->doMerge($leftElement, $rightElement); + continue; + } + } + + // if we get here then we are adding elements + // so we take all elements after the last index + // (or after -1 if we didn't enter the above loop). + $this->appendChildren($leftNode, $childName, array_slice($rightMember, ++$elementIndex)); + + // we're done with this list property, move on to + // the next property in the Node. + continue; + } + + if (is_array($leftMember) || is_array($rightMember)) { + throw new \RuntimeException('Should not happen'); + } + + // it's possible that property is NULL, if it's not then the last + // position of the last property would be used by ommission + if ($leftMember instanceof Node || $leftMember instanceof Token) { + $lastPosition = $leftMember->getStartPosition(); + } + + // if the right member is NULL then "unset" it nn the left + if ($rightMember === null && ($leftMember instanceof Node || $leftMember instanceof Token)) { + $leftNode->$childName = null; + $this->applyEdit(TextEdit::create( + $leftMember->getFullStartPosition(), + $leftMember->getFullWidth(), + '', + )); + continue; + } + + // if right member is a node and left is NULL or a node or token of a different type then replace it + if ($rightMember instanceof Node && ($leftMember === null || $leftMember::class !== $rightMember::class)) { + // update the reference content in the source node by + // applying a text edit - we'll reindex the offsets later. + $leftNode->$childName = $this->copyNode($rightMember); + $this->applyEdit(TextEdit::create( + $lastPosition, + $leftMember?->getFullWidth() ?? 0, + $rightMember->getFullText(), + )); + continue; + } + + // if we got here then the nodes are of the same class so let's + // merge them + if ($rightMember instanceof Node && $leftMember instanceof Node) { + $this->doMerge($leftMember, $rightMember); + continue; + } + + // if the right member is a token then just replace it + if ($rightMember instanceof Token) { + $replacement = $rightMember->getFullText($this->rightSource->getFileContents()); + $leftNode->$childName = $this->copyNode($rightMember); + $this->applyEdit(TextEdit::create( + $leftMember?->getFullStartPosition() ?? $lastPosition, + $leftMember?->getFullWidth() ?? 0, + $replacement, + )); + continue; + } + + // nothing and nothing is nothing + if ($rightMember === null && $leftMember === null) { + continue; + } + + throw new \RuntimeException(sprintf( + 'Do not know what to do with %s vs. %s', + get_debug_type($leftMember), get_debug_type($rightMember) + )); + } + + return; + } + + private function removeChildFrom(Node $parent, string $childName, int $index): void + { + /** @var list */ + $existingNodes = $parent->$childName; + // we need to figure out the length of the nodes we're removing + // so that we can compensate + $removedNodes = array_slice($existingNodes, $index); + + // if we didn't remove anything then there's nothing to see here. + if (count($removedNodes) === 0) { + return; + } + + // we want to truncate the source code from this point + $firstRemovedNode = $removedNodes[0]; + + // update the AST with just the nodes that were not removed + $keepNodes = array_slice($existingNodes, 0, $index); + $parent->$childName = $keepNodes; + + $removeLength = array_sum(array_map( + fn (Node|Token $node) => $node->getFullWidth() ?? 0, + $removedNodes + )); + + $this->applyEdit( + TextEdit::create( + $firstRemovedNode->getFullStartPosition(), + $removeLength, + '' + ) + ); + } + + /** + * @param list $newNodes + */ + private function appendChildren(Node $parent, string $childName, array $newNodes): void + { + $newNodes = array_map(function (Node|Token $node) { + return $this->copyNode($node); + }, $newNodes); + if (empty($newNodes)) { + return; + } + $firstNewNode = $newNodes[array_key_first($newNodes)]; + + /** @var Node[] */ + $existingNodes = $parent->$childName; + $lastExistingNode = $newNodes[array_key_last($newNodes)]; + $parent->$childName = array_merge($existingNodes, $newNodes); + + $addLength = array_sum(array_map( + fn (Node|Token $node) => $node->getFullWidth(), + $newNodes + )); + + $addContent = substr( + $this->rightSource->getFileContents(), + $firstNewNode->getFullStartPosition(), + $addLength, + ); + + $this->applyEdit( + TextEdit::create( + $lastExistingNode->getFullStartPosition(), + 0, + $addContent, + ), + ); + + } + + private function applyEdit(TextEdit $edit): void + + { + $source = $this->fileSource1; + $source->fileContents = TextEdits::one($edit)->apply($source->getFileContents()); + self::reindex($this->fileSource1); + } + + private static function reindex(Node $node): void + { + $offset = 0; + foreach ($node->getDescendantTokens() as $token) { + $leading = $token->start - $token->fullStart; + + $token->fullStart = $offset; + $token->start = $offset + $leading; + + $offset += $token->length; + } + } + + private function copyNode(Node|Token $node): Node|Token + { + return $node; + } + + /** + * @phpstan-assert null|Token|Node|list $node + */ + private static function assertNullOrNodeToken(mixed $node): void + { + if ($node === null || $node instanceof Node || $node instanceof Token || is_array($node)) { + return; + } + + throw new \RuntimeException(sprintf('Invalid node property type: %s', get_debug_type($node))); + } +} diff --git a/lib/TolerantAstDiff/Tests/Benchmark/AstDiffBench.php b/lib/TolerantAstDiff/Tests/Benchmark/AstDiffBench.php new file mode 100644 index 0000000000..59429d077d --- /dev/null +++ b/lib/TolerantAstDiff/Tests/Benchmark/AstDiffBench.php @@ -0,0 +1,24 @@ +parseSourceFile($contents, __FILE__); + + $contents = (string)substr($contents, 0, 150); + $contents .= "\n"; + $contents .= "\n"; + + $ast2 = $mergingParser->parseSourceFile($contents, __FILE__); + } +} diff --git a/lib/TolerantAstDiff/Tests/Unit/AstDiffTest.php b/lib/TolerantAstDiff/Tests/Unit/AstDiffTest.php new file mode 100644 index 0000000000..a270dc3308 --- /dev/null +++ b/lib/TolerantAstDiff/Tests/Unit/AstDiffTest.php @@ -0,0 +1,307 @@ +parseSourceFile($source); + if ($oldAst === null) { + continue; + } + $diff->merge($oldAst, $ast); + + self::assertSame($ast->getFullText(), $oldAst->getFullText(), 'AST text matches'); + self::assertSame($ast->getFullWidth(), $oldAst->getFullWidth(), 'AST width matches'); + + $oldAst = $ast; + } + + self::assertSame($source, $ast->getFullText(), 'AST content matches source'); + + if ($assertion === null) { + return; + } + + $assertion->bindTo($this)->__invoke($ast); + } + /** + * @return Generator + */ + public static function provideDiffTree(): Generator + { + yield 'same' => [ + [ + ' [ + [ + ' [ + [ + <<<'PHP' + [ + [ + ' [ + [ + ' [ + [ + ' [ + [ + ' [ + [ + ' [ + [ + <<<'PHP' + [ + [ + <<<'PHP' + [ + [ + <<<'PHP' + [ + [ + <<<'PHP' + merger->merge($node1, $node2); + dump(NodeUtil::dump($node1)); + } + PHP, + <<<'PHP' + merger->merge($node1, $node2); + dump(NodeUtil::dump($node1)); + } + PHP, + ], + function (Node $node) { + $node = $node->getDescendantNodeAtPosition(79); + self::assertEquals('$node2', $node->getText()); + }, + ]; + + yield 'node text is aligned 2' => [ + [ + <<<'PHP' + merger->merge($node1, $node2); + dump(NodeUtil::dump($node1)); + } + PHP, + <<<'PHP' + merger->merge($node1, $node2); + dump( + NodeUtil::dump($node1) + ); + } + PHP, + ], + function (Node $node) { + $node = $node->getDescendantNodeAtPosition(89); + self::assertEquals('dump', $node->getText()); + }, + ]; + + yield 'test editing session' => [ + [ + <<<'PHP' + getDescendantNodeAtPosition(48); + self::assertEquals('$node1', $node->getText()); + }, + ]; + } +} diff --git a/lib/WorseReflection/Bridge/TolerantParser/Parser/MergingParser.php b/lib/WorseReflection/Bridge/TolerantParser/Parser/MergingParser.php new file mode 100644 index 0000000000..8f660bd3a5 --- /dev/null +++ b/lib/WorseReflection/Bridge/TolerantParser/Parser/MergingParser.php @@ -0,0 +1,49 @@ + + */ + private $documents = []; + + public function __construct( + private AstDiff $merger + ) { + parent::__construct(); + } + + public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode + { + if (!$uri) { + return parent::parseSourceFile($source); + } + + if (!isset($this->documents[$uri])) { + $node = parent::parseSourceFile($source, $uri); + $this->documents[$uri] = $node; + return $node; + } + + $node1 = $this->documents[$uri]; + + $start = microtime(true); + $node2 = parent::parseSourceFile($source, $uri); + try { + $this->merger->merge($node1, $node2); + } catch (\Exception) { + return $node2; + } + + return $node1; + } +} diff --git a/lib/WorseReflection/Core/Inference/NodeContext.php b/lib/WorseReflection/Core/Inference/NodeContext.php index 5400aa5f54..1e8dfafa9f 100644 --- a/lib/WorseReflection/Core/Inference/NodeContext.php +++ b/lib/WorseReflection/Core/Inference/NodeContext.php @@ -15,6 +15,7 @@ class NodeContext * @var string[] */ private array $issues = []; + private int $nodeId = 0; protected function __construct( protected Symbol $symbol, @@ -48,6 +49,19 @@ public function withContainerType(Type $containerType): NodeContext return $new; } + public function withNodeId(int $id): self + { + $new = clone $this; + $new->nodeId = $id; + + return $new; + } + + public function nodeId(): int + { + return $this->nodeId; + } + public function withTypeAssertions(TypeAssertions $typeAssertions): NodeContext { $new = clone $this; diff --git a/lib/WorseReflection/Core/Inference/NodeContextResolver.php b/lib/WorseReflection/Core/Inference/NodeContextResolver.php index 5f1f3f87eb..bc1d248791 100644 --- a/lib/WorseReflection/Core/Inference/NodeContextResolver.php +++ b/lib/WorseReflection/Core/Inference/NodeContextResolver.php @@ -27,6 +27,8 @@ public function __construct( ) { } + public int $cacheMisses = 0; + public function withCache(Cache $cache):self { return new self($this->reflector, $this->docblockFactory, $this->logger, $cache, $this->resolverMap); @@ -83,6 +85,7 @@ private function doResolveNodeWithCache(Frame $frame, $node): NodeContext $context = $this->doResolveNode($frame, $node); $context = $context->withScope(new ReflectionScope($this->reflector, $node)); + $context = $context->withNodeId(spl_object_id($node)); return $context; }); diff --git a/lib/WorseReflection/Core/ServiceLocator.php b/lib/WorseReflection/Core/ServiceLocator.php index aa13591757..1290dc685b 100644 --- a/lib/WorseReflection/Core/ServiceLocator.php +++ b/lib/WorseReflection/Core/ServiceLocator.php @@ -64,6 +64,7 @@ public function __construct( private CacheForDocument $cacheForDocument, bool $enableContextualLocation = false, ) { + $cache = new StaticCache(); $sourceReflector = $reflectorFactory->create($this); if ($enableContextualLocation) { @@ -128,7 +129,7 @@ public function nodeContextResolver(): NodeContextResolver // use a cache which is local to this resolver instance // this avoids issues with stale cache data while also // providing memoised caching for this resolver instance. - new StaticCache(), + $this->cache, (new DefaultResolverFactory( $this->reflector, $this->nameResolver, diff --git a/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/MergingParserTest.php b/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/MergingParserTest.php new file mode 100644 index 0000000000..ac02b2fccd --- /dev/null +++ b/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/MergingParserTest.php @@ -0,0 +1,109 @@ +parseSourceFile(<<<'PHP' + getDescendantNodeAtPosition(46); + self::assertInstanceOf(EchoStatement::class, $node); + $node1OriginalId = spl_object_id($node); + $node = $ast->getDescendantNodeAtPosition(114); + self::assertInstanceOf(EchoStatement::class, $node); + $node2OriginalId = spl_object_id($node); + + // new source code introduces new line between the two nodes + $ast = $parser->parseSourceFile($source = <<<'PHP' + getText(), 'Updated AST is equal to target source'); + $node = $ast->getDescendantNodeAtPosition(46); + self::assertInstanceOf(EchoStatement::class, $node); + self::assertSame($node1OriginalId, spl_object_id($node)); + $node = $ast->getDescendantNodeAtPosition(115); + self::assertInstanceOf(EchoStatement::class, $node); + self::assertEquals($node2OriginalId, spl_object_id($node)); + + // new source code introduces a new statement + $ast = $parser->parseSourceFile($source = <<<'PHP' + getText()); + + // retrieve secnd "echo 'hello'" (before the edit) - it should be the same node as the first example + $node = $ast->getDescendantNodeAtPosition(46); + self::assertInstanceOf(EchoStatement::class, $node); + self::assertSame($node1OriginalId, spl_object_id($node)); + + // TODO: it doesn't currently supprt list inserts (e.g. 1, , 2). + // retrieve secnd "echo 'coming'" (after the edit) - it should be the same node as the first example + $node = $ast->getDescendantNodeAtPosition(132); + self::assertInstanceOf(EchoStatement::class, $node); + self::assertNotEquals($node2OriginalId, spl_object_id($node)); + } + + public function testSmokeSession(): void + { + $parser = new MergingParser(new AstDiff()); + $sources = json_decode((string)file_get_contents(__DIR__ . '/smoke.json')); + assert(is_array($sources)); + foreach ($sources as $source) { + assert(is_string($source)); + $ast = $parser->parseSourceFile($source, __FILE__); + } + $node = $ast->getDescendantNodeAtPosition(1378); + self::assertInstanceOf(Variable::class, $node); + self::assertEquals('$this', $node->getText()); + } +} diff --git a/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/smoke.json b/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/smoke.json new file mode 100644 index 0000000000..10327ed453 --- /dev/null +++ b/lib/WorseReflection/Tests/Unit/Bridge/TolerantParser/Parser/smoke.json @@ -0,0 +1,11 @@ +[ + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n $this->merger->merge($node1, $node2);\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n $this->merger->merge($node1, $node2);\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n $this->merger->merge($node1, $node2);\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n } else {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n", + "\n */\n private $documents = [];\n\n public function __construct(\n private AstDiff $merger\n ) {\n parent::__construct();\n }\n\n public function parseSourceFile(string $source, ?string $uri = null): SourceFileNode\n {\n if (!$uri) {\n return parent::parseSourceFile($source);\n }\n\n if ($uri === 'file://' . __FILE__) {\n $h = fopen('files.json', 'a+');\n fwrite($h, json_encode($source).\"\\n\");\n fclose($h);\n }\n\n if (!isset($this->documents[$uri])) {\n $node = parent::parseSourceFile($source, $uri);\n $this->documents[$uri] = deep_copy($node);\n return $node;\n }\n\n\n $node1 = $this->documents[$uri];\n\n $start = microtime(true);\n\n $node2 = parent::parseSourceFile($source, $uri);\n if (false) {\n $this->merger->merge($node1, $node2);\n } else {\n $this->merger->merge($node1, $node2);\n }\n\n return deep_copy($node1);\n }\n}\n" +] diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 252a56b825..a18ba7fdef 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" bootstrap="vendor/autoload.php" - displayDetailsOnPhpunitDeprecations="false" + displayDetailsOnPhpunitDeprecations="true" displayDetailsOnTestsThatTriggerDeprecations="true" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" diff --git a/templates/help/markdown/Phpactor/WorseReflection/Core/Inference/NodeContext.twig b/templates/help/markdown/Phpactor/WorseReflection/Core/Inference/NodeContext.twig index 7cc25985bc..b511781316 100644 --- a/templates/help/markdown/Phpactor/WorseReflection/Core/Inference/NodeContext.twig +++ b/templates/help/markdown/Phpactor/WorseReflection/Core/Inference/NodeContext.twig @@ -1 +1,2 @@ +## id: {{ object.nodeId }} {% if object.symbol.isKnown %}{{ object.symbol.symbolType }} {% endif %}{{ object.symbol.name }}{% if object.type.isDefined %}: `{{- render(object.type) -}}`{% endif %}