import type { SymbolInfo, SyntaxNode } from '../types.js'; import type { LanguageDefinition } from './types.js'; import type { LanguageTraverser, DeclarationFunctionInfo } from '../traversers/types.js'; import type { LanguageExportExtractor, LanguageImportExtractor, LanguageSymbolExtractor } from '../extractors/types.js'; /** * PHP AST traverser * * Handles PHP AST node types and traversal patterns. * PHP uses tree-sitter-php grammar. */ export declare class PHPTraverser implements LanguageTraverser { targetNodeTypes: string[]; containerTypes: string[]; declarationTypes: string[]; functionTypes: string[]; shouldExtractChildren(node: SyntaxNode): boolean; isDeclarationWithFunction(_node: SyntaxNode): boolean; getContainerBody(node: SyntaxNode): SyntaxNode | null; shouldTraverseChildren(node: SyntaxNode): boolean; findParentContainerName(node: SyntaxNode): string | undefined; findFunctionInDeclaration(_node: SyntaxNode): DeclarationFunctionInfo; } /** * PHP export extractor * * PHP doesn't have explicit export syntax. All top-level declarations are * considered exported (accessible via `use` statements): * - Classes: class User {} * - Traits: trait HasTimestamps {} * - Interfaces: interface Repository {} * - Functions: function helper() {} * - Namespaced declarations are also tracked */ export declare class PHPExportExtractor implements LanguageExportExtractor { private readonly exportableTypes; extractExports(rootNode: SyntaxNode): string[]; private extractExportsFromNode; private extractExportsFromNamespace; private extractExportableDeclaration; } /** * PHP import extractor * * Handles: * - use App\Models\User; * - use App\Services\AuthService as Auth; * - use App\Models\{User, Post as PostModel}; (grouped use, PHP 7+ — see * `firstGroupedTarget` for why only the first item is captured) */ export declare class PHPImportExtractor implements LanguageImportExtractor { readonly importNodeTypes: string[]; extractImportPath(node: SyntaxNode): string | null; extractImportPaths(node: SyntaxNode): string[]; processImportSymbols(node: SyntaxNode): { importPath: string; symbols: string[]; } | null; processImportSymbolsList(node: SyntaxNode): Array<{ importPath: string; symbols: string[]; }>; /** * Scan the WHOLE file (recursively — unlike declaration-based extraction, * which only looks at top-level `namespace_use_declaration` nodes) for * fully-qualified class-name references that never go through a `use` * statement at all. Partially addresses #878 — direct fully-qualified * references only (see below for what's still open): a test can * genuinely exercise a source class via a direct FQCN (`new * \GuzzleHttp\RetryMiddleware()`, `\GuzzleHttp\RetryMiddleware::class`) * with zero corresponding import declaration for `use`-based extraction to * find. * * Only three PHP expression shapes are considered, and only when their * class-name part is a `qualified_name` node whose own text starts with a * leading `\` (i.e. genuinely fully-qualified, resolved absolutely * regardless of any `use` imports in scope — see * `isFullyQualifiedReference`'s doc comment for why this is the * unambiguous case, unlike a bare or merely-"qualified" name): * - `new \Foo\Bar\Baz(...)` (`object_creation_expression`) * - `\Foo\Bar\Baz::class` / `\Foo\Bar\Baz::SOME_CONST` (`class_constant_access_expression`) * - `\Foo\Bar\Baz::method()` (`scoped_call_expression`) * * Deliberately does NOT attempt the transitive "factory hides the FQCN in * a different file" shape (e.g. `Middleware::retry()` from a *test* file, * where `RetryMiddleware` is only named inside `Middleware.php`, never in * the test itself) — that needs graph-level reasoning across files, well * beyond a single-file structural scan. That factory-indirection case has * no signal available at this layer and is unresolvable here; it stays an * honest, documented, still-open remainder of #878, not something this * method claims to handle. */ extractReferencedFQCNs(rootNode: SyntaxNode): string[]; private static readonly FQCN_REFERENCE_NODE_TYPES; private extractFQCNReference; /** * True when `qualifiedName` (a `qualified_name` node) is FULLY qualified — * its own source text starts with a leading `\`. PHP resolves such a name * absolutely, ignoring any `use` imports in scope, so it is unambiguous * proof the file names that exact class. * * A `qualified_name` WITHOUT the leading `\` (e.g. `Foo\Bar` inside `use * Foo\Bar::method()`) is merely "qualified": PHP resolves it relative to * the current namespace, or via an imported alias for its first segment — * genuinely ambiguous without cross-referencing the file's own namespace * and `use` imports. Treating it as a reference here would risk exactly * the false-positive shape #868/#883 guard against, so it's excluded. * * `qualified_name`'s own child structure (`namespace_name` + `name`) is * IDENTICAL whether or not the leading `\` is present — the marker exists * only in the node's own text span, not as a separate child — so this * checks `.text` directly rather than inspecting children. */ private isFullyQualifiedReference; private extractPHPUseDeclarationPath; /** * First target of a grouped use declaration's `namespace_use_group` * (`use App\Models\{User, Post as PostModel};`). tree-sitter-php parses * this shape as a `namespace_name` prefix sibling (`App\Models`) plus a * `namespace_use_group` holding one `namespace_use_clause` per item — not * the `namespace_use_clause` (with a `qualified_name` child) that the * simple/aliased form above handles, so it was previously invisible to * both `extractImportPath` and `processImportSymbols` (returned null for * the *whole* declaration, dropping every item in the group). * * Each item targets a different file under PSR-4's one-class-per-file * convention (unlike Rust's `use path::{A, B}`, where A and B share one * module/file) — so, mirroring `GoImportExtractor`'s existing "first wins" * precedent for its own multi-target grouped imports, this surfaces the * first item rather than continuing to drop the whole statement. Full * multi-target support needs a broader change (see the "grouped imports" * tracking issue) since `extractImportPath` returns one path per node. */ private firstGroupedTarget; private extractNamespacePrefix; private extractNamespaceParts; private extractQualifiedNameParts; private extractPHPQualifiedName; /** * `require`/`require_once`/`include`/`include_once` are PHP EXPRESSIONS * (they can appear as the right-hand side of an assignment), not * declarations — the grammar parses each as one of these four node types, * each wrapping exactly one child: the expression naming the file to load. */ private static readonly REQUIRE_EXPRESSION_TYPES; /** * String-literal node types that can hold a statically-readable value. * PHP's double-quoted strings parse as `encapsed_string` regardless of * whether they actually interpolate anything — `stringLiteralContent` * below is what distinguishes a plain literal from one with real * interpolation, by checking its children, not its node type. */ private static readonly STRING_LITERAL_NODE_TYPES; /** * Scan the WHOLE file (recursively, like `extractReferencedFQCNs`) for * `require`/`include` targets that are statically resolvable to a concrete * path relative to this file's own directory. See * `LanguageImportExtractor.extractStaticRequireTargets`'s doc comment for * the full contract; only three shapes are accepted here: * - A plain, non-absolute string literal (`require 'includes/foo.php';`). * - `__DIR__`/`dirname(__FILE__)` concatenated with a literal * (`require_once __DIR__ . '/../vendor/autoload.php';`). * - `dirname(__DIR__)` concatenated with a literal -- the file's PARENT * directory (`require_once dirname(__DIR__) . '/wp-load.php';`). * Everything else (a variable, a bare constant, an arbitrary function * call, an interpolated string, a ternary, ...) is left for the caller to * skip entirely — see `resolveStaticRequireTarget`. */ extractStaticRequireTargets(rootNode: SyntaxNode): string[]; /** * `node` is one of `REQUIRE_EXPRESSION_TYPES` — its sole named child is the * expression naming the file to load (optionally wrapped in one or more * `parenthesized_expression`s, e.g. WordPress's conventional * `require_once( ABSPATH . 'wp-load.php' );`). */ private extractStaticRequireTarget; private unwrapParenthesized; /** * Resolves a require/include target expression to a `./`- or `../`-prefixed * specifier, or `null` when it isn't one of the three statically-decidable * shapes this method accepts. */ private resolveStaticRequireExpression; /** * `__DIR__ . ''`, `dirname(__FILE__) . ''`, or * `dirname(__DIR__) . ''` -- PHP's idioms for "this file's own * directory" (the first two) or its PARENT (the third), each resolvable to * a `./`- or `../`-prefixed specifier relative to the file containing the * require/include statement (`resolveRelativeImport` in * `../../utils/path-matching.ts` does the actual join once this reaches * `ast/symbols.ts`). `dirname(__DIR__)` is a real, common WordPress-core * idiom for climbing from a subdirectory (`wp-admin/`) back to the install * root (confirmed on a real corpus, 19 files -- e.g. * `wp-admin/admin-ajax.php`'s `require_once dirname( __DIR__ ) . * '/wp-load.php';`) -- see `dirLevelOf`'s doc comment for why this is just * as sound as the same-directory forms, not a guess. Any other * concatenation left operand (a bare constant like `ABSPATH` -- WordPress's * OTHER common idiom, but not lexically resolvable the way a magic * constant is -- or an arbitrary function call) is not one of these forms * and is left unresolved. */ private resolveDirRelativeConcatenation; /** * Returns how many directory levels above the containing file's own * directory `node` names, or `null` when it isn't one of PHP's * `__DIR__`-equivalent forms at all: * - `__DIR__` or `dirname(__FILE__)` -- the file's own directory -- both 0. * - `dirname(__DIR__)` -- ITS PARENT -- 1. `dirname()` applied to a * directory (unlike applied to `__FILE__`, which merely strips the * filename to reach the SAME directory) genuinely climbs one level, and * `__DIR__` is always a compile-time-constant lexical value with zero * runtime ambiguity -- there is nothing to "guess" here, unlike a bare * constant such as `ABSPATH` (WordPress's other common concatenation * idiom, deliberately NOT resolved: its value is assigned dynamically in * `wp-load.php`, not lexically tied to the current file's location). * * Deliberately does not recurse into further nesting * (`dirname(dirname(__FILE__))`) -- real but rare (a handful of sites on * the same corpus that motivated the `dirname(__DIR__)` case, confined to * a single vendored library) -- left as an honest, documented remainder * rather than added speculatively. * * EXPLICITLY REJECTS (returns `null`, never guesses) PHP 8's two-argument * `dirname($path, $levels)` form -- `dirname(__DIR__, 2)` climbs TWO * levels, not one; `dirname(__FILE__, 2)` climbs one (not zero). Silently * treating it as the one-argument form would resolve to a DIFFERENT real * directory -- if a file happens to exist at that wrong path, this would * fabricate an edge to a file the statement doesn't actually require * (#928/#1008/#1056's failure mode, caught by Lien Review before it ever * shipped). Requires exactly one argument before matching `__FILE__`/ * `__DIR__` at all, below. * * Matches `__DIR__`/`__FILE__`/`dirname` case-INSENSITIVELY (confirmed * empirically against a real PHP 8.4 interpreter, #1009 Lien Review * finding): PHP's magic constants and its built-in function names are both * case-insensitive at the language level -- `__dir__`, `__Dir__`, and * `Dirname(__FILE__)` all behave identically to their canonical-case * spelling. A case-sensitive comparison would silently under-resolve any * legacy PHP file using non-canonical casing, which is exactly the kind of * codebase this fix targets -- a MISS, not a fabrication risk, since it * only means falling through to `null` (skip) rather than producing a * wrong answer. */ private dirLevelOf; /** * Returns a `string`/`encapsed_string` node's literal text content, or * `null` when it isn't a plain literal at all (interpolation present -- * `variable_name`, `expression`, etc. among its children -- makes the * value only known at runtime, not statically decidable). An empty string * literal (`''`) returns `''` rather than `null`; callers reject it as a * useless require target on their own terms. */ private stringLiteralContent; } /** * PHP symbol extractor * * Handles: * - function_definition (function foo() {}) * - method_declaration (public function bar() {}) * - class_declaration (class Foo {}) * * Call sites: function_call_expression, member_call_expression, scoped_call_expression */ export declare class PHPSymbolExtractor implements LanguageSymbolExtractor { readonly symbolNodeTypes: string[]; extractSymbol(node: SyntaxNode, content: string, parentClass?: string): SymbolInfo | null; extractCallSite(node: SyntaxNode): { symbol: string; line: number; key: string; } | null; private extractFunctionInfo; private extractMethodInfo; private extractClassInfo; } export declare const phpDefinition: LanguageDefinition; //# sourceMappingURL=php.d.ts.map