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 %}