Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | 4x 4x 2x 4x 4x 4x 6x 3x 2x 2x 2x 2x 2x 2x 2x 2x 4x 4x 2x 4x 2x 2x | /**
* Shared walk scaffolding for the tree-sitter adapters.
*
* The actual node traversal (`visit` / `walkFile`) is language-specific
* and stays in each adapter. What is duplicated — and moves here — is:
*
* - `record(out, occ)` — the occurrence-sink append.
* - `makeFileClassifier(...)` — the regex-parameterized
* `isTestFile` / `isGeneratedFile`
* predicates.
* - `runWalk({ input, walkFile })`— the `walkProject` driver skeleton:
* allocate the output sinks, filter +
* sort `input.files`, per-file
* try/catch → `ParseError`.
* - `synthesizeModuleInit(...)` — the module-init `FunctionOccurrence`
* skeleton (top-level-text join →
* `digestSyntheticBody` → occurrence).
* The `qualifiedName` shape differs per
* language and is passed in.
*/
import { relative } from 'node:path';
import type { TreeSitterParsedFile, TreeSitterParsedProject } from './parse.js';
import type {
BodyDigest,
CallSiteRecord,
DependencySiteRecord,
FunctionOccurrence,
ParseError,
WalkInput,
WalkOutput,
} from '@opensip-tools/graph';
import type Parser from 'tree-sitter';
// ── output helpers ────────────────────────────────────────────────
/** Append an occurrence into the by-simple-name occurrence sink. */
export function record(
out: Record<string, FunctionOccurrence[]>,
occ: FunctionOccurrence,
): void {
const list = out[occ.simpleName];
if (list) list.push(occ);
else out[occ.simpleName] = [occ];
}
// ── file classification ───────────────────────────────────────────
/** Regex inputs for the shared file classifier. */
export interface FileClassifierConfig {
/** Matches a test file by name (e.g. `*_test.go`, `*Test.java`). */
readonly testRe: RegExp;
/** Matches a generated / build-output path. */
readonly generatedRe: RegExp;
/**
* Optional path-based test matcher (e.g. `src/test/`, `tests/`). When
* present, a file is a test file if EITHER `testPathRe` or `testRe`
* matches. Go omits this (the `_test.go` name convention is exact);
* java/python/rust supply it.
*/
readonly testPathRe?: RegExp;
}
/** The bound file-classification predicates. */
export interface FileClassifier {
readonly isTestFile: (rel: string) => boolean;
readonly isGeneratedFile: (rel: string) => boolean;
}
/** Builds `isTestFile` / `isGeneratedFile` bound to the given regexes. */
export function makeFileClassifier(config: FileClassifierConfig): FileClassifier {
const { testRe, generatedRe, testPathRe } = config;
return {
isTestFile: testPathRe === undefined
? (rel: string): boolean => testRe.test(rel)
: (rel: string): boolean => testPathRe.test(rel) || testRe.test(rel),
isGeneratedFile: (rel: string): boolean => generatedRe.test(rel),
};
}
// ── walk driver ───────────────────────────────────────────────────
/** Inputs to the shared `walkProject` driver. */
export interface RunWalkParams<P extends TreeSitterParsedProject> {
readonly input: WalkInput<P>;
/**
* The adapter's per-file walk: visits one file's AST and pushes
* occurrences / call sites / dependency sites into the supplied sinks.
*/
readonly walkFile: (
absPath: string,
file: P['files'] extends ReadonlyMap<string, infer F> ? F : never,
projectDirAbs: string,
out: Record<string, FunctionOccurrence[]>,
callSites: CallSiteRecord[],
dependencySites: DependencySiteRecord[],
) => void;
}
/**
* Drives `walkProject`: allocate the output sinks, iterate
* `input.files` (filtered to parsed files, sorted for I-1 determinism),
* and run `walkFile` per file with a try/catch that records a
* `ParseError` on failure.
*/
export function runWalk<P extends TreeSitterParsedProject>(
params: RunWalkParams<P>,
): WalkOutput {
const { input, walkFile } = params;
const occurrences: Record<string, FunctionOccurrence[]> = Object.create(null) as Record<
string,
FunctionOccurrence[]
>;
const callSites: CallSiteRecord[] = [];
const dependencySites: DependencySiteRecord[] = [];
const parseErrors: ParseError[] = [];
const sortedPaths = [...input.files].filter((p) => input.project.files.has(p)).sort();
for (const path of sortedPaths) {
const file = input.project.files.get(path);
if (!file) continue;
try {
walkFile(
path,
file as P['files'] extends ReadonlyMap<string, infer F> ? F : never,
input.projectDirAbs,
occurrences,
callSites,
dependencySites,
);
} catch (error) {
parseErrors.push({
filePath: relative(input.projectDirAbs, path),
message: error instanceof Error ? error.message : String(error),
});
}
}
return { occurrences, callSites, dependencySites, parseErrors };
}
// ── module-init synthesis ─────────────────────────────────────────
/** Inputs to the shared module-init occurrence builder. */
export interface SynthesizeModuleInitParams<F extends TreeSitterParsedFile> {
readonly file: F;
readonly filePathProjectRel: string;
readonly inTestFile: boolean;
readonly definedInGenerated: boolean;
/** The adapter's synthetic-body digest (same normalization as real bodies). */
readonly digestSyntheticBody: (text: string) => BodyDigest;
/**
* The fully-built `qualifiedName` for the synthetic `<module-init>`
* occurrence — language-specific (extension strip + separator differ:
* `pkg/file.<module-init>` for go, `a.b.<module-init>` for python,
* `a::b::<module-init>` for rust, …). Computed by the adapter.
*/
readonly qualifiedName: string;
}
/**
* Build the synthetic `<module-init>` `FunctionOccurrence`: hash the
* file's top-level statement-text concatenation and assemble the
* occurrence with the adapter-supplied `qualifiedName`.
*/
export function synthesizeModuleInit<F extends TreeSitterParsedFile>(
params: SynthesizeModuleInitParams<F>,
): FunctionOccurrence {
const { file, filePathProjectRel, inTestFile, definedInGenerated, digestSyntheticBody, qualifiedName } =
params;
const root: Parser.SyntaxNode = file.tree.rootNode;
const topLevelText = root.children
.map((c) => file.source.slice(c.startIndex, c.endIndex))
.join('\n');
const digest = digestSyntheticBody(`${filePathProjectRel}\n${topLevelText}`);
return {
bodyHash: digest.hash,
bodySize: digest.size,
simpleName: `<module-init:${filePathProjectRel}>`,
qualifiedName,
filePath: filePathProjectRel,
line: 1,
column: 0,
endLine: root.endPosition.row + 1,
kind: 'module-init',
params: [],
returnType: null,
enclosingClass: null,
decorators: [],
visibility: 'module-local',
inTestFile,
definedInGenerated,
calls: [],
};
}
// ── resolver helper ───────────────────────────────────────────────
/**
* Build a simple-name → bodyHash[] index from the walk's occurrence map.
* Skips synthetic names (those starting with `<`); only real names are
* resolution targets. Language-agnostic (operates on the engine's
* `FunctionOccurrence` record), so it is shared by every resolver that
* needs it (go / java / python; rust resolves differently).
*/
export function buildNameIndex(
functions: Readonly<Record<string, readonly FunctionOccurrence[]>>,
): ReadonlyMap<string, readonly string[]> {
const out = new Map<string, string[]>();
for (const [name, occs] of Object.entries(functions)) {
Iif (!occs) continue;
if (name.startsWith('<')) continue;
const list: string[] = out.get(name) ?? [];
for (const o of occs) list.push(o.bodyHash);
Eif (list.length > 0) out.set(name, list);
}
return out;
}
|