Vendored WhatsApp/tree-sitter-erlang 0.19 (the ELP grammar, ABI 14) with an Erlang-shaped extractor: multi-clause/multi-arity functions merged into one symbol, -spec signatures, records with fields, -type/-opaque aliases, -define macros, -include/-include_lib file edges, and -export-driven visibility. Modules wrap in a namespace so remote mod:fn(...) calls resolve through the existing qualified-name matcher as mod::fn with zero resolver changes. -behaviour declarations link to the behaviour module — gated to namespace targets only (bare-name fallthrough linked -behaviour(supervisor) to an unrelated macro constant on emqx). OTP indirection with static targets is followed: spawn/apply/proc_lib/timer/rpc MFA-argument callees, and gen_server:call/cast(?MODULE | ?SERVER) to the module's own handle_call/handle_cast. Var-module dispatch and message sends stay deliberately unlinked. codegraph_explore also normalizes Erlang-native query spelling (mod:fn/3, init/2) so named symbols resolve as typed. Benchmarked on cowboy (189 files), ejabberd (414), emqx (2,447): extraction PASS on all three; with-codegraph arms reached 2/2/0 file Reads vs 10/5+/19 without, fastest on the largest repo. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
/**
|
|
* Tree-sitter Shared Helpers
|
|
*
|
|
* Utility functions used by the core TreeSitterExtractor and per-language extractors.
|
|
* Extracted to a leaf module to avoid circular imports between tree-sitter.ts and languages/.
|
|
*/
|
|
|
|
import { Node as SyntaxNode } from 'web-tree-sitter';
|
|
import * as crypto from 'crypto';
|
|
import { NodeKind } from '../types';
|
|
|
|
/**
|
|
* Generate a unique node ID
|
|
*
|
|
* Uses a 32-character (128-bit) hash to avoid collisions when indexing
|
|
* large codebases with many files containing similar symbols.
|
|
*/
|
|
export function generateNodeId(
|
|
filePath: string,
|
|
kind: NodeKind,
|
|
name: string,
|
|
line: number
|
|
): string {
|
|
const hash = crypto
|
|
.createHash('sha256')
|
|
.update(`${filePath}:${kind}:${name}:${line}`)
|
|
.digest('hex')
|
|
.substring(0, 32);
|
|
return `${kind}:${hash}`;
|
|
}
|
|
|
|
/**
|
|
* Extract text from a syntax node
|
|
*/
|
|
export function getNodeText(node: SyntaxNode, source: string): string {
|
|
return source.substring(node.startIndex, node.endIndex);
|
|
}
|
|
|
|
/**
|
|
* Find a child node by field name
|
|
*/
|
|
export function getChildByField(node: SyntaxNode, fieldName: string): SyntaxNode | null {
|
|
return node.childForFieldName(fieldName);
|
|
}
|
|
|
|
/**
|
|
* Node types that *wrap* a declaration so a leading comment is a sibling of the
|
|
* wrapper, not of the emitted (inner) declaration node. CodeGraph emits the
|
|
* inner node, so before looking for its preceding comment we climb out through
|
|
* these. Examples: `export class X {}` (export_statement), `@dec\ndef f()`
|
|
* (decorated_definition), `const f = () => {}` (lexical_declaration →
|
|
* variable_declarator). Each wraps exactly one declaration, so climbing can't
|
|
* mis-attribute a comment to a sibling. (#780)
|
|
*/
|
|
const DOCSTRING_WRAPPER_TYPES = new Set([
|
|
'export_statement', // JS/TS: export class/function/const ...
|
|
'decorated_definition', // Python: @decorator over def/class
|
|
'lexical_declaration', // JS/TS: const/let x = () => {}
|
|
'variable_declaration', // JS/TS: var x = ...
|
|
'variable_declarator', // JS/TS: the `x = () => {}` inside the declaration
|
|
'ambient_declaration', // TS: declare ...
|
|
]);
|
|
|
|
/**
|
|
* Strip comment-syntax markers from a raw comment so the stored docstring is
|
|
* just the prose. Covers the marker styles across every supported language:
|
|
* C-family line and block comments and their doc variants, Rust/Swift/Kotlin
|
|
* triple-slash and bang doc lines, hash lines (Python/Ruby/shell), Lua/Luau
|
|
* line and long-bracket comments, and Pascal brace and paren-star comments.
|
|
* (#780)
|
|
*
|
|
* Paired block delimiters are stripped only when the comment OPENS with one,
|
|
* so a line comment that merely happens to END with a closing delimiter is
|
|
* never truncated. The per-line markers are anchored at line start, so
|
|
* they're safe to apply to any comment.
|
|
*/
|
|
function cleanCommentMarkers(comment: string): string {
|
|
let c = comment.trim();
|
|
if (c.startsWith('/*')) c = c.replace(/^\/\*+!?/, '').replace(/\*+\/$/, '');
|
|
else if (c.startsWith('--[')) c = c.replace(/^--\[=*\[/, '').replace(/\]=*\]$/, '');
|
|
else if (c.startsWith('(*')) c = c.replace(/^\(\*/, '').replace(/\*\)$/, '');
|
|
else if (c.startsWith('{')) c = c.replace(/^\{/, '').replace(/\}$/, '');
|
|
return c
|
|
.replace(/^\/\/[/!]?\s?/gm, '') // // , and Rust/Swift doc lines /// //!
|
|
.replace(/^--\s?/gm, '') // Lua/Luau line comments
|
|
.replace(/^#\s?/gm, '') // Python/Ruby/shell line comments
|
|
.replace(/^%+\s?/gm, '') // Erlang line comments (% / %% / %%%)
|
|
.replace(/^\s*\*\s?/gm, '') // block-comment continuation (* foo)
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* Get the docstring/comment preceding a node
|
|
*/
|
|
export function getPrecedingDocstring(node: SyntaxNode, source: string): string | undefined {
|
|
// Climb out of any wrapper(s) so a comment preceding the WHOLE construct
|
|
// (export-, decorator-, or const-arrow-wrapped) is reachable as a sibling.
|
|
// The emitted node's own `previousNamedSibling` is empty (export/const) or a
|
|
// decorator (Python) in those cases, so without this the docstring was
|
|
// dropped. (#780)
|
|
let anchor = node;
|
|
while (anchor.parent && DOCSTRING_WRAPPER_TYPES.has(anchor.parent.type)) {
|
|
anchor = anchor.parent;
|
|
}
|
|
|
|
let sibling = anchor.previousNamedSibling;
|
|
const comments: string[] = [];
|
|
|
|
while (sibling) {
|
|
if (
|
|
sibling.type === 'comment' ||
|
|
sibling.type === 'line_comment' ||
|
|
sibling.type === 'block_comment' ||
|
|
sibling.type === 'documentation_comment'
|
|
) {
|
|
comments.unshift(getNodeText(sibling, source));
|
|
sibling = sibling.previousNamedSibling;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (comments.length === 0) return undefined;
|
|
|
|
// Strip each comment's syntax markers (language-aware), then join.
|
|
return comments.map(cleanCommentMarkers).join('\n').trim();
|
|
}
|