parser = (new ParserFactory())->createParser(); $this->printer = new Printer(); $this->cleaner = $cleaner; } /** * Analyze input and return the coarse parser-derived context. */ public function analyze(string $input, int $cursor, array $readlineInfo = []): AnalysisResult { // Cursor is in code-point units, so use mb_substr $inputToCursor = \mb_substr($input, 0, $cursor); $cursorAtEnd = ($cursor >= \mb_strlen($input)); $analysis = $this->tryParse($inputToCursor, $cursorAtEnd); $parseSucceeded = $analysis !== null; $analysis = $analysis ?? new AnalysisResult(CompletionKind::UNKNOWN, ''); $analysis->parseSucceeded = $parseSucceeded; $analysis->input = $inputToCursor; $analysis->tokens = @\token_get_all('readlineInfo = $readlineInfo; return $analysis; } /** * Try to parse input and analyze the resulting AST. */ private function tryParse(string $input, bool $cursorAtEnd): ?AnalysisResult { $code = 'parser->parse($code.';'); } catch (PhpParserError $e) { try { $stmts = $this->parser->parse($code); } catch (PhpParserError $e2) { return null; } } if (empty($stmts)) { return new AnalysisResult(CompletionKind::UNKNOWN, ''); } $visitor = new DeepestNodeVisitor(); $traverser = new NodeTraverser(); $traverser->addVisitor($visitor); $traverser->traverse($stmts); $node = $visitor->getDeepestNode(); if ($node === null) { return new AnalysisResult(CompletionKind::UNKNOWN, ''); } // Trailing whitespace means the user has moved past this token if ($cursorAtEnd && $input !== \rtrim($input)) { return new AnalysisResult(CompletionKind::UNKNOWN, ''); } return $this->analyzeNode($node); } /** * Analyze a specific AST node to determine completion context. */ private function analyzeNode(Node $node): AnalysisResult { // $foo->bar, $foo->bar(), $foo?->bar, $foo?->bar() if ( $node instanceof MethodCall || $node instanceof PropertyFetch || $node instanceof NullsafeMethodCall || $node instanceof NullsafePropertyFetch ) { $leftSide = $this->extractExpression($node->var); $prefix = $node->name instanceof Identifier ? $node->name->name : ''; $result = new AnalysisResult(CompletionKind::OBJECT_MEMBER, $prefix, $leftSide); $result->leftSideNode = $node->var; return $result; } // Foo::bar(), Foo::BAR, Foo::$bar if ( $node instanceof StaticCall || $node instanceof ClassConstFetch || $node instanceof StaticPropertyFetch ) { $leftSide = $this->extractExpression($node->class); $prefix = $node->name instanceof Identifier ? $node->name->name : ''; $result = new AnalysisResult(CompletionKind::STATIC_MEMBER, $prefix, $leftSide); $result->leftSideNode = $node->class; return $result; } // new Foo if ($node instanceof New_) { $prefix = $node->class instanceof Name ? $node->class->toString() : ''; return new AnalysisResult(CompletionKind::CLASS_NAME, $prefix); } // $foo if ($node instanceof Variable) { $prefix = \is_string($node->name) ? $node->name : ''; return new AnalysisResult(CompletionKind::VARIABLE, $prefix); } // foo() if ($node instanceof FuncCall && $node->name instanceof Name) { return new AnalysisResult(CompletionKind::FUNCTION_NAME, $node->name->toString()); } // FOO (could be a constant, function, or class reference) if ($node instanceof ConstFetch && $node->name instanceof Name) { return new AnalysisResult(CompletionKind::SYMBOL, $node->name->toString()); } if ($node instanceof Identifier) { return new AnalysisResult(CompletionKind::UNKNOWN, $node->name); } // Bare name: foo or Foo\Bar if ($node instanceof Name) { return new AnalysisResult(CompletionKind::SYMBOL, $node->toString()); } return new AnalysisResult(CompletionKind::UNKNOWN, ''); } /** * Extract a string representation of an expression. * * Uses the printer to convert the AST node back to code. */ private function extractExpression($expr): string { if ($expr instanceof Variable && \is_string($expr->name)) { return '$'.$expr->name; } if ($expr instanceof Name) { return $this->resolveClassName($expr); } if ($expr instanceof Node\Expr) { return $this->printer->prettyPrintExpr($expr); } return ''; } /** * Resolve a class name using CodeCleaner's namespace context. * * This ensures we use the same use statements and namespace as the code * being executed. */ private function resolveClassName(Name $name): string { if ($this->cleaner === null) { return $name->toString(); } return $this->cleaner->resolveClassName($name->toString()); } }