brainclaw 1.25.0 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +1 -4
- package/dist/commands/mcp.js +7 -7
- package/dist/commands/session-start.js +137 -15
- package/dist/core/bootstrap.js +28 -4
- package/dist/core/code-map/aggregate.js +36 -31
- package/dist/core/code-map/backend.js +4 -4
- package/dist/core/code-map/core.js +1 -0
- package/dist/core/code-map/export.js +4 -4
- package/dist/core/code-map/finalizer.js +57 -2
- package/dist/core/code-map/freshness.js +78 -13
- package/dist/core/code-map/impact.js +36 -4
- package/dist/core/code-map/indexes.js +37 -0
- package/dist/core/code-map/lang/python/index.js +4 -2
- package/dist/core/code-map/lang/query-runtime.js +2 -0
- package/dist/core/code-map/lang/typescript/index.js +4 -2
- package/dist/core/code-map/lang/usages.js +333 -0
- package/dist/core/code-map/memory-reader.js +15 -0
- package/dist/core/code-map/query.js +209 -58
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +27 -2
- package/dist/core/code-map/store.js +1 -0
- package/dist/core/code-map/types.js +55 -9
- package/dist/core/code-map/vocabulary.js +6 -0
- package/dist/core/code-map/work-section.js +12 -14
- package/dist/core/context-diff.js +17 -3
- package/dist/core/entity-operations.js +14 -2
- package/dist/core/hint-aging.js +4 -1
- package/dist/core/identity.js +284 -91
- package/dist/core/io.js +192 -0
- package/dist/core/project-discovery.js +7 -1
- package/dist/core/runtime.js +99 -11
- package/dist/core/store-resolution.js +5 -21
- package/dist/facts.js +12 -12
- package/dist/facts.json +11 -11
- package/docs/code-map.md +36 -27
- package/package.json +1 -1
|
@@ -45,9 +45,72 @@ export function coarseFreshness(status) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Build the canonical, surface-uniform badge. `freshness` is derived solely from
|
|
50
|
+
* the index state supplied as `status`; a query's bounded spot-check is diagnostic
|
|
51
|
+
* evidence under `details.spot_check`, never a competing top-level badge.
|
|
52
|
+
*/
|
|
53
|
+
export function makeFreshnessBadge(status, options = {}) {
|
|
54
|
+
const spot = options.spotCheck ?? {};
|
|
55
|
+
return {
|
|
56
|
+
freshness: coarseFreshness(status),
|
|
57
|
+
status,
|
|
58
|
+
details: {
|
|
59
|
+
...(options.extra ?? {}),
|
|
60
|
+
index: {
|
|
61
|
+
status,
|
|
62
|
+
stale_file_count: options.staleFileCount ?? 0,
|
|
63
|
+
partial_reason: options.partialReason ?? null,
|
|
64
|
+
git_head_changed: options.gitHeadChanged ?? null,
|
|
65
|
+
},
|
|
66
|
+
spot_check: {
|
|
67
|
+
status: spot.status ?? 'not_run',
|
|
68
|
+
checked_files: spot.checked_files ?? 0,
|
|
69
|
+
stale_changed_files: spot.stale_changed_files ?? [],
|
|
70
|
+
deleted_files: spot.deleted_files ?? [],
|
|
71
|
+
unchecked_files: spot.unchecked_files ?? [],
|
|
72
|
+
budget_exhausted: spot.budget_exhausted ?? false,
|
|
73
|
+
partial_reason: spot.partial_reason ?? null,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compatibility normalizer for internal callers that previously constructed a
|
|
80
|
+
* `{ status, details }` badge. It preserves non-freshness metadata while always
|
|
81
|
+
* adding the two canonical detail sections.
|
|
82
|
+
*/
|
|
83
|
+
export function withFreshness(b) {
|
|
84
|
+
const raw = b.details ?? {};
|
|
85
|
+
const index = raw.index;
|
|
86
|
+
const spot = raw.spot_check;
|
|
87
|
+
const known = new Set([
|
|
88
|
+
'index', 'spot_check', 'stale_file_count', 'partial_reason', 'git_head_changed',
|
|
89
|
+
'stale_changed_files', 'deleted_files', 'unchecked_files', 'budget',
|
|
90
|
+
]);
|
|
91
|
+
const extra = Object.fromEntries(Object.entries(raw).filter(([key]) => !known.has(key)));
|
|
92
|
+
const stringArray = (value) => Array.isArray(value) ? value.map(String).sort() : [];
|
|
93
|
+
const numberValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
94
|
+
const nullableString = (value) => typeof value === 'string' ? value : value === null ? null : undefined;
|
|
95
|
+
const git = (index?.git_head_changed ?? raw.git_head_changed);
|
|
96
|
+
const gitHeadChanged = git && typeof git.index_head === 'string' && typeof git.current_head === 'string'
|
|
97
|
+
? { index_head: git.index_head, current_head: git.current_head }
|
|
98
|
+
: null;
|
|
99
|
+
return makeFreshnessBadge(b.status, {
|
|
100
|
+
staleFileCount: numberValue(index?.stale_file_count ?? raw.stale_file_count),
|
|
101
|
+
partialReason: nullableString(index?.partial_reason ?? raw.partial_reason),
|
|
102
|
+
gitHeadChanged,
|
|
103
|
+
spotCheck: {
|
|
104
|
+
status: spot?.status,
|
|
105
|
+
checked_files: numberValue(spot?.checked_files),
|
|
106
|
+
stale_changed_files: stringArray(spot?.stale_changed_files ?? raw.stale_changed_files),
|
|
107
|
+
deleted_files: stringArray(spot?.deleted_files ?? raw.deleted_files),
|
|
108
|
+
unchecked_files: stringArray(spot?.unchecked_files ?? raw.unchecked_files),
|
|
109
|
+
budget_exhausted: spot?.budget_exhausted === true,
|
|
110
|
+
partial_reason: nullableString(spot?.partial_reason),
|
|
111
|
+
},
|
|
112
|
+
extra,
|
|
113
|
+
});
|
|
51
114
|
}
|
|
52
115
|
/** Stable serialization: sort object keys recursively so hashing is order-independent. */
|
|
53
116
|
function stableStringify(value) {
|
|
@@ -165,16 +228,18 @@ export function summarizeFreshness(shards) {
|
|
|
165
228
|
* actionable status; only the cause detail is added.
|
|
166
229
|
*/
|
|
167
230
|
export function applyGitHeadDrift(badge, indexHead, currentHead) {
|
|
231
|
+
const normalized = withFreshness(badge);
|
|
232
|
+
const currentIndex = normalized.details.index;
|
|
168
233
|
if (!indexHead || !currentHead || indexHead === currentHead)
|
|
169
|
-
return
|
|
170
|
-
const status =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
234
|
+
return normalized;
|
|
235
|
+
const status = normalized.status === 'fresh' ? 'stale_git_head' : normalized.status;
|
|
236
|
+
const extra = Object.fromEntries(Object.entries(normalized.details).filter(([key]) => key !== 'index' && key !== 'spot_check'));
|
|
237
|
+
return makeFreshnessBadge(status, {
|
|
238
|
+
staleFileCount: currentIndex.stale_file_count,
|
|
239
|
+
partialReason: currentIndex.partial_reason,
|
|
240
|
+
gitHeadChanged: { index_head: indexHead, current_head: currentHead },
|
|
241
|
+
spotCheck: normalized.details.spot_check,
|
|
242
|
+
extra,
|
|
243
|
+
});
|
|
179
244
|
}
|
|
180
245
|
//# sourceMappingURL=freshness.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileId } from './ids.js';
|
|
11
|
+
import { makeFreshnessBadge } from './freshness.js';
|
|
11
12
|
import { readManifest, readResolutionIndex, readShard, readSymbolsIndex, } from './store.js';
|
|
12
13
|
import { deriveBadge, isTestPath, makeLazyChecker, newAccumulator, validateStoreEntry, } from './query.js';
|
|
13
14
|
/** Direct results and each optional transitive layer are independently bounded. */
|
|
@@ -100,12 +101,14 @@ function causeKey(cause) {
|
|
|
100
101
|
cause.target.kind,
|
|
101
102
|
cause.target.path,
|
|
102
103
|
cause.target.node_id ?? '',
|
|
104
|
+
cause.caller?.node_id ?? '',
|
|
103
105
|
].join('\u0001');
|
|
104
106
|
}
|
|
105
107
|
function compareCause(a, b) {
|
|
106
108
|
return a.kind.localeCompare(b.kind)
|
|
107
109
|
|| a.target.path.localeCompare(b.target.path)
|
|
108
110
|
|| (a.target.node_id ?? '').localeCompare(b.target.node_id ?? '')
|
|
111
|
+
|| (a.caller?.node_id ?? '').localeCompare(b.caller?.node_id ?? '')
|
|
109
112
|
|| (a.module ?? '').localeCompare(b.module ?? '')
|
|
110
113
|
|| (a.source_line ?? -1) - (b.source_line ?? -1)
|
|
111
114
|
|| a.imported.join('\u0000').localeCompare(b.imported.join('\u0000'))
|
|
@@ -138,6 +141,33 @@ function addRelation(rows, entry, depth, target, kind) {
|
|
|
138
141
|
current.causes.sort(compareCause);
|
|
139
142
|
rows.set(entry.path, current);
|
|
140
143
|
}
|
|
144
|
+
function addUsageRelation(rows, entry, depth, target) {
|
|
145
|
+
const current = rows.get(entry.path) ?? {
|
|
146
|
+
path: entry.path,
|
|
147
|
+
file_id: entry.file_id,
|
|
148
|
+
depth,
|
|
149
|
+
causes: [],
|
|
150
|
+
causeKeys: new Set(),
|
|
151
|
+
};
|
|
152
|
+
current.depth = Math.min(current.depth, depth);
|
|
153
|
+
for (const reason of entry.reasons) {
|
|
154
|
+
const cause = {
|
|
155
|
+
kind: reason.kind,
|
|
156
|
+
imported: [],
|
|
157
|
+
confidence: reason.confidence,
|
|
158
|
+
...(reason.source_line !== undefined ? { source_line: reason.source_line } : {}),
|
|
159
|
+
caller: { node_id: reason.caller_node_id },
|
|
160
|
+
target,
|
|
161
|
+
};
|
|
162
|
+
const key = causeKey(cause);
|
|
163
|
+
if (!current.causeKeys.has(key)) {
|
|
164
|
+
current.causeKeys.add(key);
|
|
165
|
+
current.causes.push(cause);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
current.causes.sort(compareCause);
|
|
169
|
+
rows.set(entry.path, current);
|
|
170
|
+
}
|
|
141
171
|
function publicRelation(row) {
|
|
142
172
|
return {
|
|
143
173
|
path: row.path,
|
|
@@ -205,7 +235,7 @@ export function impact(target, options, ctx) {
|
|
|
205
235
|
freshness_badge: freshness,
|
|
206
236
|
});
|
|
207
237
|
if (!symbolsIndex || !manifest) {
|
|
208
|
-
return empty(
|
|
238
|
+
return empty(makeFreshnessBadge('missing_index', { extra: { hint: 'run refresh' } }));
|
|
209
239
|
}
|
|
210
240
|
let matchKind = 'none';
|
|
211
241
|
let rawDefinitions = [];
|
|
@@ -257,10 +287,12 @@ export function impact(target, options, ctx) {
|
|
|
257
287
|
const directRows = new Map();
|
|
258
288
|
if (resolution) {
|
|
259
289
|
for (const definition of definitionByNodeId.values()) {
|
|
290
|
+
const target = { kind: 'symbol', path: definition.path, node_id: definition.node_id, name: definition.name };
|
|
260
291
|
for (const dependent of resolution.dependents_by_symbol[definition.node_id] ?? []) {
|
|
261
|
-
addRelation(directRows, dependent, 1,
|
|
262
|
-
|
|
263
|
-
|
|
292
|
+
addRelation(directRows, dependent, 1, target, 'imports_symbol');
|
|
293
|
+
}
|
|
294
|
+
for (const dependent of resolution.usages_by_symbol[definition.node_id] ?? []) {
|
|
295
|
+
addUsageRelation(directRows, dependent, 1, target);
|
|
264
296
|
}
|
|
265
297
|
}
|
|
266
298
|
for (const definitionPath of definitionPaths) {
|
|
@@ -130,6 +130,29 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
130
130
|
// target key -> (importer path -> merged entry)
|
|
131
131
|
const byFile = new Map();
|
|
132
132
|
const bySymbol = new Map();
|
|
133
|
+
const byUsageSymbol = new Map();
|
|
134
|
+
const addUsage = (targetSymbolId, importerPath, importerFileId, edge) => {
|
|
135
|
+
const perImporter = byUsageSymbol.get(targetSymbolId) ?? new Map();
|
|
136
|
+
const entry = perImporter.get(importerPath) ?? { path: importerPath, file_id: importerFileId, reasons: [] };
|
|
137
|
+
const reason = {
|
|
138
|
+
kind: edge.kind,
|
|
139
|
+
caller_node_id: edge.from,
|
|
140
|
+
confidence: edge.confidence,
|
|
141
|
+
...(edge.source?.line !== undefined ? { source_line: edge.source.line } : {}),
|
|
142
|
+
};
|
|
143
|
+
if (!entry.reasons.some((existing) => existing.kind === reason.kind
|
|
144
|
+
&& existing.caller_node_id === reason.caller_node_id
|
|
145
|
+
&& existing.confidence === reason.confidence
|
|
146
|
+
&& existing.source_line === reason.source_line)) {
|
|
147
|
+
entry.reasons.push(reason);
|
|
148
|
+
entry.reasons.sort((a, b) => a.kind.localeCompare(b.kind)
|
|
149
|
+
|| a.caller_node_id.localeCompare(b.caller_node_id)
|
|
150
|
+
|| (a.source_line ?? -1) - (b.source_line ?? -1)
|
|
151
|
+
|| a.confidence - b.confidence);
|
|
152
|
+
}
|
|
153
|
+
perImporter.set(importerPath, entry);
|
|
154
|
+
byUsageSymbol.set(targetSymbolId, perImporter);
|
|
155
|
+
};
|
|
133
156
|
const addDependent = (bucket, targetKey, importerPath, importerFileId, module, imported, confidence, kind, sourceLine) => {
|
|
134
157
|
const perImporter = bucket.get(targetKey) ?? new Map();
|
|
135
158
|
const prev = perImporter.get(importerPath);
|
|
@@ -189,6 +212,12 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
189
212
|
moduleById.set(n.id, { name: n.name, imported: n.imported_names ?? [] });
|
|
190
213
|
}
|
|
191
214
|
for (const e of shard.edges) {
|
|
215
|
+
if (e.kind === 'calls' || e.kind === 'references') {
|
|
216
|
+
// `possible_textual_match` remains deliberately absent: it is a hint on
|
|
217
|
+
// the shard, never an impact dependency.
|
|
218
|
+
addUsage(e.to, shard.path, shard.file_id, { ...e, kind: e.kind });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
192
221
|
if (e.kind !== 'resolves_to' && e.kind !== 'imports_symbol')
|
|
193
222
|
continue;
|
|
194
223
|
const mod = moduleById.get(e.from);
|
|
@@ -210,12 +239,20 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
210
239
|
}
|
|
211
240
|
return out;
|
|
212
241
|
};
|
|
242
|
+
const finalizeUsages = (bucket) => {
|
|
243
|
+
const out = Object.create(null);
|
|
244
|
+
for (const key of [...bucket.keys()].sort()) {
|
|
245
|
+
out[key] = [...bucket.get(key).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
213
249
|
return {
|
|
214
250
|
schema_version: CODE_MAP_SCHEMA_VERSION,
|
|
215
251
|
project_id: projectId,
|
|
216
252
|
updated_at: new Date().toISOString(),
|
|
217
253
|
dependents_by_file: finalize(byFile),
|
|
218
254
|
dependents_by_symbol: finalize(bySymbol),
|
|
255
|
+
usages_by_symbol: finalizeUsages(byUsageSymbol),
|
|
219
256
|
};
|
|
220
257
|
}
|
|
221
258
|
//# sourceMappingURL=indexes.js.map
|
|
@@ -30,6 +30,7 @@ import path from 'node:path';
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { loadGrammarWasm, grammarHashForWasm } from '../../wasm-loader.js';
|
|
32
32
|
import { extractWithQueries } from '../query-runtime.js';
|
|
33
|
+
import { extractLexicalUsages } from '../usages.js';
|
|
33
34
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
34
35
|
/** The python grammar .wasm: dist basename + node_modules devDep fallback spec. */
|
|
35
36
|
const PY_WASM_BASENAME = 'tree-sitter-python.wasm';
|
|
@@ -106,7 +107,7 @@ const queries = {
|
|
|
106
107
|
};
|
|
107
108
|
const vocabulary = {
|
|
108
109
|
nodeSubtypes: ['function', 'method', 'class', 'variable', 'constant', 'property'],
|
|
109
|
-
edgeKinds: ['contains', 'defines', 'imports'],
|
|
110
|
+
edgeKinds: ['contains', 'defines', 'imports', 'calls', 'references', 'possible_textual_match'],
|
|
110
111
|
captureMap: queries.captureMap,
|
|
111
112
|
};
|
|
112
113
|
const capabilities = {
|
|
@@ -284,7 +285,8 @@ export class PythonProvider {
|
|
|
284
285
|
}
|
|
285
286
|
return d;
|
|
286
287
|
});
|
|
287
|
-
|
|
288
|
+
const tree = draft.attributes?.__tree;
|
|
289
|
+
return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'python') };
|
|
288
290
|
}
|
|
289
291
|
/**
|
|
290
292
|
* P1c file-level import resolution (T3). Returns at most one resolution per import:
|
|
@@ -121,6 +121,7 @@ export async function extractWithQueries(input) {
|
|
|
121
121
|
imports: [],
|
|
122
122
|
exports: [],
|
|
123
123
|
tests: [],
|
|
124
|
+
usages: [],
|
|
124
125
|
facts,
|
|
125
126
|
attributes: { parseStatus, __tree: tree },
|
|
126
127
|
});
|
|
@@ -367,6 +368,7 @@ export async function extractWithQueries(input) {
|
|
|
367
368
|
imports,
|
|
368
369
|
exports,
|
|
369
370
|
tests: [],
|
|
371
|
+
usages: [],
|
|
370
372
|
facts,
|
|
371
373
|
attributes: { parseStatus, __tree: tree },
|
|
372
374
|
};
|
|
@@ -26,6 +26,7 @@ import path from 'node:path';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { grammarHash, grammarName, loadGrammar } from '../../wasm-loader.js';
|
|
28
28
|
import { extractWithQueries } from '../query-runtime.js';
|
|
29
|
+
import { extractLexicalUsages } from '../usages.js';
|
|
29
30
|
import { isTypeScriptResolutionConfig, typeScriptSpecifierBases, } from './config.js';
|
|
30
31
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
/** Resolve a vendored `.scm` next to this module (dist) or from the source tree. */
|
|
@@ -150,7 +151,7 @@ const queries = {
|
|
|
150
151
|
};
|
|
151
152
|
const vocabulary = {
|
|
152
153
|
nodeSubtypes: ['function', 'class', 'type', 'interface', 'variable', 'component', 'hook', 'export'],
|
|
153
|
-
edgeKinds: ['contains', 'defines', 'imports', 'exports'],
|
|
154
|
+
edgeKinds: ['contains', 'defines', 'imports', 'exports', 'calls', 'references', 'possible_textual_match'],
|
|
154
155
|
captureMap: queries.captureMap,
|
|
155
156
|
};
|
|
156
157
|
const capabilities = {
|
|
@@ -305,7 +306,8 @@ export class TypeScriptProvider {
|
|
|
305
306
|
}
|
|
306
307
|
return d;
|
|
307
308
|
});
|
|
308
|
-
|
|
309
|
+
const tree = draft.attributes?.__tree;
|
|
310
|
+
return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'js-ts') };
|
|
309
311
|
}
|
|
310
312
|
/**
|
|
311
313
|
* P1c file-level import resolution (intra-project, relative specifiers). Returns
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
const FUNCTION_SUBTYPES = new Set(['function', 'component', 'hook']);
|
|
2
|
+
const LOW_CONFIDENCE_TEXTUAL_MATCH = 0.2;
|
|
3
|
+
function sourceOf(definition) {
|
|
4
|
+
const value = definition.sourceNode;
|
|
5
|
+
if (!value || typeof value !== 'object' || !('node' in value) || !('nameNode' in value))
|
|
6
|
+
return null;
|
|
7
|
+
const candidate = value;
|
|
8
|
+
return candidate.node && candidate.nameNode ? candidate : null;
|
|
9
|
+
}
|
|
10
|
+
function spanOf(node) {
|
|
11
|
+
return {
|
|
12
|
+
start_line: node.startPosition.row + 1,
|
|
13
|
+
start_col: node.startPosition.column + 1,
|
|
14
|
+
end_line: node.endPosition.row + 1,
|
|
15
|
+
end_col: node.endPosition.column + 1,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function nodeKey(node) {
|
|
19
|
+
return `${node.type}:${node.startIndex}:${node.endIndex}`;
|
|
20
|
+
}
|
|
21
|
+
function sameNode(a, b) {
|
|
22
|
+
return !!a && a.type === b.type && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
|
|
23
|
+
}
|
|
24
|
+
function field(node, name) {
|
|
25
|
+
try {
|
|
26
|
+
return node.childForFieldName(name);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function walk(node, visit) {
|
|
33
|
+
visit(node);
|
|
34
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
35
|
+
const child = node.namedChild(i);
|
|
36
|
+
if (child)
|
|
37
|
+
walk(child, visit);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function walkScope(node, rootScope, language, visit) {
|
|
41
|
+
visit(node);
|
|
42
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
43
|
+
const child = node.namedChild(i);
|
|
44
|
+
if (!child)
|
|
45
|
+
continue;
|
|
46
|
+
// Bindings in a nested function/class/lambda cannot shadow a use in this
|
|
47
|
+
// scope. Skipping them avoids false abstention between sibling functions.
|
|
48
|
+
if (child !== rootScope && isScope(child, language))
|
|
49
|
+
continue;
|
|
50
|
+
walkScope(child, rootScope, language, visit);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function hasAncestor(node, types) {
|
|
54
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
55
|
+
if (types.has(parent.type))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function isTopLevelPythonFunction(definition) {
|
|
61
|
+
const source = sourceOf(definition);
|
|
62
|
+
if (!source)
|
|
63
|
+
return false;
|
|
64
|
+
let parent = source.node.parent;
|
|
65
|
+
if (parent?.type === 'decorated_definition')
|
|
66
|
+
parent = parent.parent;
|
|
67
|
+
return parent?.type === 'module';
|
|
68
|
+
}
|
|
69
|
+
function isLocalFunctionTarget(definition, language) {
|
|
70
|
+
if (!FUNCTION_SUBTYPES.has(definition.subtype))
|
|
71
|
+
return false;
|
|
72
|
+
// JS/TS tags are already program-anchored. Python captures nested functions too,
|
|
73
|
+
// so retain only module bindings: nested/method names need a real scope engine.
|
|
74
|
+
return language === 'js-ts' || isTopLevelPythonFunction(definition);
|
|
75
|
+
}
|
|
76
|
+
function isCallerDefinition(definition) {
|
|
77
|
+
return FUNCTION_SUBTYPES.has(definition.subtype);
|
|
78
|
+
}
|
|
79
|
+
function definitionRange(definition) {
|
|
80
|
+
const source = sourceOf(definition);
|
|
81
|
+
if (!source)
|
|
82
|
+
return null;
|
|
83
|
+
// A lexical declaration can carry several declarators. The declarator, not the
|
|
84
|
+
// shared statement span, is the smallest safe caller container for an arrow.
|
|
85
|
+
return source.nameNode.parent ?? source.node;
|
|
86
|
+
}
|
|
87
|
+
function contains(container, node) {
|
|
88
|
+
return container.startIndex <= node.startIndex && container.endIndex >= node.endIndex;
|
|
89
|
+
}
|
|
90
|
+
function callerFor(node, definitions) {
|
|
91
|
+
let winner;
|
|
92
|
+
for (const definition of definitions) {
|
|
93
|
+
if (!isCallerDefinition(definition))
|
|
94
|
+
continue;
|
|
95
|
+
const range = definitionRange(definition);
|
|
96
|
+
if (!range || !contains(range, node))
|
|
97
|
+
continue;
|
|
98
|
+
const width = range.endIndex - range.startIndex;
|
|
99
|
+
if (!winner || width < winner.width || (width === winner.width && definition.ordinal > winner.ordinal)) {
|
|
100
|
+
winner = { ordinal: definition.ordinal, width };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return winner?.ordinal;
|
|
104
|
+
}
|
|
105
|
+
function isScope(node, language) {
|
|
106
|
+
if (language === 'python')
|
|
107
|
+
return ['module', 'function_definition', 'lambda', 'class_definition'].includes(node.type);
|
|
108
|
+
return ['program', 'function_declaration', 'function_expression', 'arrow_function', 'method_definition'].includes(node.type);
|
|
109
|
+
}
|
|
110
|
+
function isParameterIdentifier(node, language) {
|
|
111
|
+
const parameterTypes = language === 'python'
|
|
112
|
+
? new Set(['parameters', 'lambda_parameters'])
|
|
113
|
+
: new Set(['formal_parameters', 'required_parameter', 'optional_parameter', 'rest_pattern']);
|
|
114
|
+
return hasAncestor(node, parameterTypes);
|
|
115
|
+
}
|
|
116
|
+
function isBindingIdentifier(node, language) {
|
|
117
|
+
if (node.type !== 'identifier')
|
|
118
|
+
return false;
|
|
119
|
+
const parent = node.parent;
|
|
120
|
+
if (!parent)
|
|
121
|
+
return false;
|
|
122
|
+
if (sameNode(field(parent, 'name'), node) && [
|
|
123
|
+
'variable_declarator', 'function_declaration', 'generator_function_declaration',
|
|
124
|
+
'function_definition', 'class_declaration', 'class_definition', 'aliased_import',
|
|
125
|
+
].includes(parent.type))
|
|
126
|
+
return true;
|
|
127
|
+
if (sameNode(field(parent, 'left'), node) && [
|
|
128
|
+
'assignment', 'augmented_assignment', 'for_statement', 'with_item',
|
|
129
|
+
].includes(parent.type))
|
|
130
|
+
return true;
|
|
131
|
+
return isParameterIdentifier(node, language);
|
|
132
|
+
}
|
|
133
|
+
function scopeBinds(scope, name, language, allowed) {
|
|
134
|
+
let found = false;
|
|
135
|
+
walkScope(scope, scope, language, (candidate) => {
|
|
136
|
+
if (found || candidate.text !== name || !isBindingIdentifier(candidate, language))
|
|
137
|
+
return;
|
|
138
|
+
if (!allowed.has(nodeKey(candidate)))
|
|
139
|
+
found = true;
|
|
140
|
+
});
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
function isShadowed(node, name, language, allowed) {
|
|
144
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
145
|
+
if (isScope(parent, language) && scopeBinds(parent, name, language, allowed))
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function propertyOf(node) {
|
|
151
|
+
return field(node, 'property') ?? field(node, 'attribute');
|
|
152
|
+
}
|
|
153
|
+
function isProperty(node) {
|
|
154
|
+
const parent = node.parent;
|
|
155
|
+
return !!parent && sameNode(propertyOf(parent), node)
|
|
156
|
+
&& ['member_expression', 'optional_member_expression', 'attribute'].includes(parent.type);
|
|
157
|
+
}
|
|
158
|
+
function isValueReference(node, language) {
|
|
159
|
+
if (node.type !== 'identifier' || isBindingIdentifier(node, language))
|
|
160
|
+
return false;
|
|
161
|
+
const parent = node.parent;
|
|
162
|
+
if (!parent || isProperty(node))
|
|
163
|
+
return false;
|
|
164
|
+
if (hasAncestor(node, new Set(['import_statement', 'import_from_statement'])))
|
|
165
|
+
return false;
|
|
166
|
+
if (sameNode(field(parent, 'key'), node) || ['export_specifier', 'export_clause'].includes(parent.type))
|
|
167
|
+
return false;
|
|
168
|
+
// Type positions have distinct namespaces in TS and do not prove a value-level use.
|
|
169
|
+
if (hasAncestor(node, new Set(['type_annotation', 'type_alias_declaration', 'interface_declaration', 'type_identifier'])))
|
|
170
|
+
return false;
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function addBinding(bindings, localName, binding) {
|
|
174
|
+
const previous = bindings.get(localName);
|
|
175
|
+
bindings.set(localName, previous === undefined ? binding : null);
|
|
176
|
+
}
|
|
177
|
+
function stripQuotes(text) {
|
|
178
|
+
return text.replace(/^['"`]|['"`]$/g, '');
|
|
179
|
+
}
|
|
180
|
+
function jsTsBindings(root) {
|
|
181
|
+
const bindings = new Map();
|
|
182
|
+
walk(root, (statement) => {
|
|
183
|
+
if (statement.type !== 'import_statement')
|
|
184
|
+
return;
|
|
185
|
+
const source = field(statement, 'source');
|
|
186
|
+
if (!source)
|
|
187
|
+
return;
|
|
188
|
+
const module = stripQuotes(source.text);
|
|
189
|
+
let clause = null;
|
|
190
|
+
for (let i = 0; i < statement.namedChildCount; i++) {
|
|
191
|
+
const child = statement.namedChild(i);
|
|
192
|
+
if (child?.type === 'import_clause') {
|
|
193
|
+
clause = child;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!clause)
|
|
198
|
+
return;
|
|
199
|
+
walk(clause, (candidate) => {
|
|
200
|
+
if (candidate.type !== 'import_specifier')
|
|
201
|
+
return;
|
|
202
|
+
const imported = field(candidate, 'name');
|
|
203
|
+
if (!imported || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported.text))
|
|
204
|
+
return;
|
|
205
|
+
const alias = field(candidate, 'alias');
|
|
206
|
+
addBinding(bindings, (alias ?? imported).text, {
|
|
207
|
+
module,
|
|
208
|
+
importedName: imported.text,
|
|
209
|
+
bindingNode: alias ?? imported,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
return bindings;
|
|
214
|
+
}
|
|
215
|
+
function pythonBindings(root) {
|
|
216
|
+
const bindings = new Map();
|
|
217
|
+
walk(root, (statement) => {
|
|
218
|
+
if (statement.type !== 'import_from_statement')
|
|
219
|
+
return;
|
|
220
|
+
const source = field(statement, 'module_name');
|
|
221
|
+
if (!source)
|
|
222
|
+
return;
|
|
223
|
+
const module = source.text;
|
|
224
|
+
for (let i = 0; i < statement.namedChildCount; i++) {
|
|
225
|
+
const child = statement.namedChild(i);
|
|
226
|
+
if (!child || sameNode(child, source))
|
|
227
|
+
continue;
|
|
228
|
+
if (child.type === 'dotted_name' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(child.text)) {
|
|
229
|
+
addBinding(bindings, child.text, { module, importedName: child.text, bindingNode: child });
|
|
230
|
+
}
|
|
231
|
+
else if (child.type === 'aliased_import') {
|
|
232
|
+
const imported = field(child, 'name');
|
|
233
|
+
const alias = field(child, 'alias');
|
|
234
|
+
if (imported && alias && /^[A-Za-z_][A-Za-z0-9_]*$/.test(imported.text)) {
|
|
235
|
+
addBinding(bindings, alias.text, { module, importedName: imported.text, bindingNode: alias });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return bindings;
|
|
241
|
+
}
|
|
242
|
+
/** Extract provider-local, soundness-first usage drafts from an already parsed tree. */
|
|
243
|
+
export function extractLexicalUsages(root, definitions, language) {
|
|
244
|
+
if (!root)
|
|
245
|
+
return [];
|
|
246
|
+
const localTargets = new Map();
|
|
247
|
+
const allowedBindings = new Set();
|
|
248
|
+
for (const definition of definitions) {
|
|
249
|
+
if (!isLocalFunctionTarget(definition, language))
|
|
250
|
+
continue;
|
|
251
|
+
const previous = localTargets.get(definition.name);
|
|
252
|
+
localTargets.set(definition.name, previous === undefined ? definition : null);
|
|
253
|
+
const source = sourceOf(definition);
|
|
254
|
+
if (source)
|
|
255
|
+
allowedBindings.add(nodeKey(source.nameNode));
|
|
256
|
+
}
|
|
257
|
+
const bindings = language === 'js-ts' ? jsTsBindings(root) : pythonBindings(root);
|
|
258
|
+
for (const binding of bindings.values())
|
|
259
|
+
if (binding)
|
|
260
|
+
allowedBindings.add(nodeKey(binding.bindingNode));
|
|
261
|
+
const usages = [];
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
const handledCallIdentifiers = new Set();
|
|
264
|
+
const handledProperties = new Set();
|
|
265
|
+
const add = (node, kind, target) => {
|
|
266
|
+
const caller = callerFor(node, definitions);
|
|
267
|
+
const targetKey = target.kind === 'local'
|
|
268
|
+
? `local:${target.definitionOrdinal}`
|
|
269
|
+
: `import:${target.module}:${target.importedName}`;
|
|
270
|
+
const key = `${nodeKey(node)}:${kind}:${caller ?? 'file'}:${targetKey}`;
|
|
271
|
+
if (seen.has(key))
|
|
272
|
+
return;
|
|
273
|
+
seen.add(key);
|
|
274
|
+
usages.push({
|
|
275
|
+
kind,
|
|
276
|
+
...(caller === undefined ? {} : { fromDefinitionOrdinal: caller }),
|
|
277
|
+
target,
|
|
278
|
+
span: spanOf(node),
|
|
279
|
+
confidence: kind === 'possible_textual_match' ? LOW_CONFIDENCE_TEXTUAL_MATCH : 1.0,
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
const directUse = (node, kind) => {
|
|
283
|
+
const name = node.text;
|
|
284
|
+
const local = localTargets.get(name);
|
|
285
|
+
const imported = bindings.get(name);
|
|
286
|
+
if ((local === undefined || local === null) && (!imported || imported === null))
|
|
287
|
+
return;
|
|
288
|
+
if (isShadowed(node, name, language, allowedBindings))
|
|
289
|
+
return;
|
|
290
|
+
if (local && local !== null) {
|
|
291
|
+
add(node, kind, { kind: 'local', definitionOrdinal: local.ordinal });
|
|
292
|
+
}
|
|
293
|
+
else if (imported) {
|
|
294
|
+
add(node, kind, { kind: 'import', module: imported.module, importedName: imported.importedName });
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(root, (node) => {
|
|
298
|
+
if (node.type !== 'call_expression' && node.type !== 'call')
|
|
299
|
+
return;
|
|
300
|
+
const callee = field(node, 'function');
|
|
301
|
+
if (callee?.type === 'identifier') {
|
|
302
|
+
handledCallIdentifiers.add(nodeKey(callee));
|
|
303
|
+
directUse(callee, 'calls');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!callee)
|
|
307
|
+
return;
|
|
308
|
+
const property = propertyOf(callee);
|
|
309
|
+
const local = property ? localTargets.get(property.text) : undefined;
|
|
310
|
+
if (property && local && local !== null) {
|
|
311
|
+
handledProperties.add(nodeKey(property));
|
|
312
|
+
add(property, 'possible_textual_match', { kind: 'local', definitionOrdinal: local.ordinal });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
walk(root, (node) => {
|
|
316
|
+
if (node.type !== 'identifier')
|
|
317
|
+
return;
|
|
318
|
+
if (handledCallIdentifiers.has(nodeKey(node)) || handledProperties.has(nodeKey(node)))
|
|
319
|
+
return;
|
|
320
|
+
if (isProperty(node)) {
|
|
321
|
+
const local = localTargets.get(node.text);
|
|
322
|
+
if (local && local !== null)
|
|
323
|
+
add(node, 'possible_textual_match', { kind: 'local', definitionOrdinal: local.ordinal });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (isValueReference(node, language))
|
|
327
|
+
directUse(node, 'references');
|
|
328
|
+
});
|
|
329
|
+
return usages.sort((a, b) => a.span.start_line - b.span.start_line
|
|
330
|
+
|| a.span.start_col - b.span.start_col
|
|
331
|
+
|| a.kind.localeCompare(b.kind));
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=usages.js.map
|