Files
codegraph/src/extraction/tree-sitter-types.ts
T
38eb4e688c fix(extraction): classify TS/JS class fields by value — properties, not methods (#808) (#809)
Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).

Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).

With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.

A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).

EXTRACTION_VERSION 19 → 20 (re-index to benefit).

Closes #808

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:48:11 -05:00

264 lines
11 KiB
TypeScript

/**
* Tree-sitter Extraction Types
*
* Defines the LanguageExtractor interface and related types used by
* the core TreeSitterExtractor and per-language extraction configs.
* Extracted to a leaf module to avoid circular imports.
*/
import { Node as SyntaxNode } from 'web-tree-sitter';
import {
Node,
NodeKind,
UnresolvedReference,
} from '../types';
/**
* Information returned by a language's extractImport hook.
*/
export interface ImportInfo {
/** The module/package name being imported */
moduleName: string;
/** Full import statement text for display */
signature: string;
/** If true, the hook already created unresolved references itself */
handledRefs?: boolean;
}
/**
* Information about a single variable within a declaration.
* Returned by a language's extractVariables hook.
*/
export interface VariableInfo {
/** Variable name */
name: string;
/** Node kind: 'variable' or 'constant' */
kind: NodeKind;
/** Optional signature string */
signature?: string;
/** If set, this declarator is actually a function and should be extracted as such */
delegateToFunction?: SyntaxNode;
/** The AST node to use for positioning (may differ from the declaration node) */
positionNode?: SyntaxNode;
}
/**
* Context object passed to language hooks that need to call back into the core extractor.
* Provides a controlled API surface — hooks can create nodes, visit children, and add
* references without accessing the full TreeSitterExtractor internals.
*/
export interface ExtractorContext {
/** Create a node and add it to the extraction result */
createNode(kind: NodeKind, name: string, node: SyntaxNode, extra?: Partial<Node>): Node | null;
/** Visit a child node (dispatches through the standard visitNode logic) */
visitNode(node: SyntaxNode): void;
/** Visit a function body to extract calls */
visitFunctionBody(body: SyntaxNode, functionId: string): void;
/** Add an unresolved reference */
addUnresolvedReference(ref: UnresolvedReference): void;
/** Push a node ID onto the scope stack (for containment/qualified name building) */
pushScope(nodeId: string): void;
/** Pop the last node ID from the scope stack */
popScope(): void;
/** Current file path */
readonly filePath: string;
/** Current source text */
readonly source: string;
/** Stack of parent node IDs (current scope) */
readonly nodeStack: readonly string[];
/** All nodes extracted so far */
readonly nodes: readonly Node[];
}
/**
* Language-specific extraction configuration.
*
* Each supported language provides an implementation of this interface
* that configures which AST node types to look for and how to extract
* language-specific details like signatures, visibility, and imports.
*/
export interface LanguageExtractor {
/**
* Optional source transform applied immediately before the grammar parses the
* file. Used to work around grammar gaps that would otherwise corrupt the
* parse tree (e.g. C# blanks conditional-compilation directive lines the
* grammar mis-parses inside enum bodies). MUST preserve byte offsets (replace
* removed text with spaces, keep newlines) so node positions and getNodeText
* stay correct; the returned string is used for both parsing and extraction.
*/
preParse?: (source: string) => string;
// --- Node type mappings ---
/** Node types that represent functions */
functionTypes: string[];
/** Node types that represent classes */
classTypes: string[];
/** Node types that represent methods */
methodTypes: string[];
/** Node types that represent interfaces/protocols/traits */
interfaceTypes: string[];
/** Node types that represent structs */
structTypes: string[];
/** Node types that represent enums */
enumTypes: string[];
/** Node types that represent enum members/cases (e.g. Swift: 'enum_entry', Rust: 'enum_variant') */
enumMemberTypes?: string[];
/** Node types that represent type aliases (e.g. `type X = ...`) */
typeAliasTypes: string[];
/** Node types that represent imports */
importTypes: string[];
/** Node types that represent function calls */
callTypes: string[];
/** Node types that represent variable declarations (const, let, var, etc.) */
variableTypes: string[];
/** Node types that represent class fields (extracted as 'field' kind inside class bodies) */
fieldTypes?: string[];
/** Node types that represent class properties (extracted as 'property' kind inside class bodies) */
propertyTypes?: string[];
// --- Field name mappings ---
/** Field name for identifier/name */
nameField: string;
/** Field name for body */
bodyField: string;
/** Field name for parameters */
paramsField: string;
/** Field name for return type */
returnField?: string;
// --- Existing hooks ---
/** Override symbol name extraction (e.g. ObjC multi-part selectors). */
resolveName?: (node: SyntaxNode, source: string) => string | undefined;
/** Extract property name when the generic name walk fails (e.g. ObjC @property). */
extractPropertyName?: (node: SyntaxNode, source: string) => string | null;
/** Extract signature from node */
getSignature?: (node: SyntaxNode, source: string) => string | undefined;
/** Extract visibility from node */
getVisibility?: (node: SyntaxNode) => 'public' | 'private' | 'protected' | 'internal' | undefined;
/** Check if node is exported */
isExported?: (node: SyntaxNode, source: string) => boolean;
/** Check if node is async */
isAsync?: (node: SyntaxNode) => boolean;
/** Check if node is static */
isStatic?: (node: SyntaxNode) => boolean;
/** Check if variable declaration is a constant (const vs let/var) */
isConst?: (node: SyntaxNode) => boolean;
/**
* Extract extra symbol-level modifier keywords to persist on the node's
* `decorators` list (e.g. Kotlin `expect`/`actual` multiplatform markers).
* Called generically for every created node; return undefined/[] when none.
* Used by the resolver to link `expect` declarations to their `actual`
* implementations across source sets.
*/
extractModifiers?: (node: SyntaxNode) => string[] | undefined;
// --- New config properties ---
/** Additional node types to treat as class declarations (e.g. Dart: 'mixin_declaration') */
extraClassNodeTypes?: string[];
/** Whether methods can be top-level without enclosing class (Go: true) */
methodsAreTopLevel?: boolean;
/** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
interfaceKind?: NodeKind;
// --- New hooks ---
/**
* Custom node visitor. Return true if the node was fully handled (skip default dispatch).
* Used by languages with fundamentally different AST structures (e.g. Pascal).
*/
visitNode?: (node: SyntaxNode, ctx: ExtractorContext) => boolean;
/**
* Classify a class_declaration node when the grammar reuses one node type
* for multiple concepts (e.g. Swift uses class_declaration for classes, structs, and enums).
*/
classifyClassNode?: (node: SyntaxNode) => 'class' | 'struct' | 'enum' | 'interface' | 'trait';
/**
* Classify a methodTypes node when the grammar reuses one node type for
* both callable and data members (#808): TS/JS class FIELDS
* (`public_field_definition` / `field_definition`) are methods only when
* their value is callable (`onClick = () => {}`); a plain field
* (`public fonts: Fonts;`, `count = 0`) is a property. Default: 'method'.
*/
classifyMethodNode?: (node: SyntaxNode) => 'method' | 'property';
/**
* Resolve the body node for a function/method/class when it's not a child field.
* (e.g. Dart puts function_body as a sibling, not a child.)
*/
resolveBody?: (node: SyntaxNode, bodyField: string) => SyntaxNode | null;
/**
* Extract import information from an import node.
* Return null if the node isn't a recognized import form.
*/
extractImport?: (node: SyntaxNode, source: string) => ImportInfo | null;
/**
* Extract variable declarations from a variable declaration node.
* Returns info about each declared variable, allowing the core to create nodes.
*/
extractVariables?: (node: SyntaxNode, source: string) => VariableInfo[];
/**
* Extract receiver/owner type name from a method declaration.
* Used by Go to get the struct receiver (e.g., "scrapeLoop" from "func (sl *scrapeLoop) run()").
* When present, the receiver type is included in the qualified name for better searchability.
*/
getReceiverType?: (node: SyntaxNode, source: string) => string | undefined;
/**
* Extract a function/method's normalized return type name (bare class name,
* smart-pointer pointee unwrapped), stored on the node as `returnType`. Used
* by C/C++ so resolution can infer a chained receiver's type from what the
* inner call returns (`Foo::instance().bar()` → resolve `bar` on `Foo`,
* issue #645). Return undefined for primitives / void / constructors.
*/
getReturnType?: (node: SyntaxNode, source: string) => string | undefined;
/**
* Resolve the actual node kind for a type alias declaration.
* Used by Go where `type_spec` is the named declaration wrapper for structs/interfaces:
* `type Foo struct { ... }` → type_spec (name: "Foo") → struct_type
* Returns 'struct', 'interface', etc. to override the default 'type_alias' kind,
* or undefined to keep it as a type alias.
*/
resolveTypeAliasKind?: (node: SyntaxNode, source: string) => NodeKind | undefined;
/**
* Check if a function/method name is a misparse artifact that should be skipped.
* Used by C/C++ where macros (e.g. NLOHMANN_JSON_NAMESPACE_BEGIN) cause tree-sitter
* to misparse namespace blocks as function_definitions. When this returns true,
* the function node is NOT created, but the body is still visited for calls and
* structural nodes (classes, structs, enums).
*/
isMisparsedFunction?: (name: string, node: SyntaxNode) => boolean;
/**
* Detect bare method calls that don't use call expression syntax.
* Used by Ruby where `reset` (no parens, no receiver) is a method call but
* tree-sitter parses it as a plain `identifier` node instead of `call`/`method_call`.
* Returns the callee name if this node is a bare call, or undefined if not.
*/
extractBareCall?: (node: SyntaxNode, source: string) => string | undefined;
/**
* Node types representing a file-level package/namespace declaration
* (e.g. Kotlin `package_header`, Java `package_declaration`). When set,
* the core wraps every top-level declaration in an implicit `namespace`
* node carrying the FQN, so cross-file import resolution can match by
* qualifiedName instead of filename (Kotlin filename ≠ class name).
*/
packageTypes?: string[];
/** Extract the dotted package name from a package declaration node. */
extractPackage?: (node: SyntaxNode, source: string) => string | null;
}