gitnexus 1.6.5-rc.13 → 1.6.5-rc.15
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/core/incremental/shadow-candidates.d.ts +44 -0
- package/dist/core/incremental/shadow-candidates.js +74 -0
- package/dist/core/incremental/subgraph-extract.d.ts +64 -0
- package/dist/core/incremental/subgraph-extract.js +111 -0
- package/dist/core/ingestion/call-processor.js +163 -45
- package/dist/core/ingestion/community-processor.js +18 -0
- package/dist/core/ingestion/languages/java/arity-metadata.d.ts +18 -0
- package/dist/core/ingestion/languages/java/arity-metadata.js +40 -0
- package/dist/core/ingestion/languages/java/arity.d.ts +10 -0
- package/dist/core/ingestion/languages/java/arity.js +24 -0
- package/dist/core/ingestion/languages/java/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/java/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/java/captures.d.ts +17 -0
- package/dist/core/ingestion/languages/java/captures.js +187 -0
- package/dist/core/ingestion/languages/java/import-decomposer.d.ts +18 -0
- package/dist/core/ingestion/languages/java/import-decomposer.js +85 -0
- package/dist/core/ingestion/languages/java/import-target.d.ts +17 -0
- package/dist/core/ingestion/languages/java/import-target.js +100 -0
- package/dist/core/ingestion/languages/java/index.d.ts +29 -0
- package/dist/core/ingestion/languages/java/index.js +29 -0
- package/dist/core/ingestion/languages/java/interpret.d.ts +13 -0
- package/dist/core/ingestion/languages/java/interpret.js +131 -0
- package/dist/core/ingestion/languages/java/merge-bindings.d.ts +12 -0
- package/dist/core/ingestion/languages/java/merge-bindings.js +40 -0
- package/dist/core/ingestion/languages/java/query.d.ts +30 -0
- package/dist/core/ingestion/languages/java/query.js +192 -0
- package/dist/core/ingestion/languages/java/receiver-binding.d.ts +11 -0
- package/dist/core/ingestion/languages/java/receiver-binding.js +95 -0
- package/dist/core/ingestion/languages/java/scope-resolver.d.ts +50 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +74 -0
- package/dist/core/ingestion/languages/java/simple-hooks.d.ts +13 -0
- package/dist/core/ingestion/languages/java/simple-hooks.js +34 -0
- package/dist/core/ingestion/languages/java.js +11 -0
- package/dist/core/ingestion/parsing-processor.d.ts +22 -2
- package/dist/core/ingestion/parsing-processor.js +83 -49
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +5 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +119 -11
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +14 -0
- package/dist/core/ingestion/pipeline.d.ts +13 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +13 -1
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +20 -4
- package/dist/core/lbug/lbug-adapter.d.ts +26 -0
- package/dist/core/lbug/lbug-adapter.js +70 -0
- package/dist/core/run-analyze.js +318 -28
- package/dist/storage/file-hash.d.ts +47 -0
- package/dist/storage/file-hash.js +86 -0
- package/dist/storage/parse-cache.d.ts +67 -0
- package/dist/storage/parse-cache.js +182 -0
- package/dist/storage/repo-manager.d.ts +41 -1
- package/dist/storage/repo-manager.js +17 -2
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-candidate path derivation for incremental indexing.
|
|
3
|
+
*
|
|
4
|
+
* Background — Bugbot review on PR #1479:
|
|
5
|
+
* queryImporters() on a NEWLY ADDED file returns 0 importers in the
|
|
6
|
+
* pre-pipeline DB, because the new file's IMPORTS rows haven't been
|
|
7
|
+
* written yet. But pre-existing files may have IMPORTS edges that
|
|
8
|
+
* *resolved to a sibling path*, and the newcomer can now steal that
|
|
9
|
+
* resolution under standard JS/TS module-resolution rules. Without
|
|
10
|
+
* pulling those pre-existing files into the writable set, their
|
|
11
|
+
* stale CALLS edges remain pointing at the OLD resolution target.
|
|
12
|
+
*
|
|
13
|
+
* Given an added file path, this helper enumerates the pre-existing
|
|
14
|
+
* file paths whose import-resolution claim the newcomer can steal.
|
|
15
|
+
* Caller filters the candidates against the prior-run `fileHashes`
|
|
16
|
+
* map so we only query importers of paths that actually existed.
|
|
17
|
+
*
|
|
18
|
+
* Shadow patterns covered (resolution-priority-aware):
|
|
19
|
+
*
|
|
20
|
+
* (a) Same basename, different extension —
|
|
21
|
+
* added `foo/bar.ts` shadows `foo/bar.{tsx,js,jsx,mjs,cjs,d.ts}`.
|
|
22
|
+
* (b) Bare-file beats directory-style index —
|
|
23
|
+
* added `foo/bar.ts` shadows `foo/bar/index.{ts,tsx,...}`.
|
|
24
|
+
* (c) Directory-index beats bare-file —
|
|
25
|
+
* added `foo/index.ts` shadows `foo.{ts,tsx,...}` (rare but real,
|
|
26
|
+
* e.g. converting a single-file module into a directory module).
|
|
27
|
+
*
|
|
28
|
+
* Resolution-order priority is conservatively wide: we enumerate ALL
|
|
29
|
+
* common extensions because we don't know which the importer actually
|
|
30
|
+
* specified, and over-seeding is harmless (extra BFS work, but the
|
|
31
|
+
* subgraph extract still gates write-back by file membership).
|
|
32
|
+
*
|
|
33
|
+
* Cross-platform path separators: candidates are emitted with both `/`
|
|
34
|
+
* and `\` for shadow pattern (b), since the caller's prior fileHashes
|
|
35
|
+
* map may use either depending on the OS that wrote it.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Enumerate pre-existing paths whose import-resolution `added` can steal.
|
|
39
|
+
*
|
|
40
|
+
* @param added — repo-relative path of a newly-added file
|
|
41
|
+
* @returns deduplicated list of candidate paths (NOT filtered against
|
|
42
|
+
* any known-files set — caller does that)
|
|
43
|
+
*/
|
|
44
|
+
export declare const shadowCandidatesFor: (added: string) => string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-candidate path derivation for incremental indexing.
|
|
3
|
+
*
|
|
4
|
+
* Background — Bugbot review on PR #1479:
|
|
5
|
+
* queryImporters() on a NEWLY ADDED file returns 0 importers in the
|
|
6
|
+
* pre-pipeline DB, because the new file's IMPORTS rows haven't been
|
|
7
|
+
* written yet. But pre-existing files may have IMPORTS edges that
|
|
8
|
+
* *resolved to a sibling path*, and the newcomer can now steal that
|
|
9
|
+
* resolution under standard JS/TS module-resolution rules. Without
|
|
10
|
+
* pulling those pre-existing files into the writable set, their
|
|
11
|
+
* stale CALLS edges remain pointing at the OLD resolution target.
|
|
12
|
+
*
|
|
13
|
+
* Given an added file path, this helper enumerates the pre-existing
|
|
14
|
+
* file paths whose import-resolution claim the newcomer can steal.
|
|
15
|
+
* Caller filters the candidates against the prior-run `fileHashes`
|
|
16
|
+
* map so we only query importers of paths that actually existed.
|
|
17
|
+
*
|
|
18
|
+
* Shadow patterns covered (resolution-priority-aware):
|
|
19
|
+
*
|
|
20
|
+
* (a) Same basename, different extension —
|
|
21
|
+
* added `foo/bar.ts` shadows `foo/bar.{tsx,js,jsx,mjs,cjs,d.ts}`.
|
|
22
|
+
* (b) Bare-file beats directory-style index —
|
|
23
|
+
* added `foo/bar.ts` shadows `foo/bar/index.{ts,tsx,...}`.
|
|
24
|
+
* (c) Directory-index beats bare-file —
|
|
25
|
+
* added `foo/index.ts` shadows `foo.{ts,tsx,...}` (rare but real,
|
|
26
|
+
* e.g. converting a single-file module into a directory module).
|
|
27
|
+
*
|
|
28
|
+
* Resolution-order priority is conservatively wide: we enumerate ALL
|
|
29
|
+
* common extensions because we don't know which the importer actually
|
|
30
|
+
* specified, and over-seeding is harmless (extra BFS work, but the
|
|
31
|
+
* subgraph extract still gates write-back by file membership).
|
|
32
|
+
*
|
|
33
|
+
* Cross-platform path separators: candidates are emitted with both `/`
|
|
34
|
+
* and `\` for shadow pattern (b), since the caller's prior fileHashes
|
|
35
|
+
* map may use either depending on the OS that wrote it.
|
|
36
|
+
*/
|
|
37
|
+
const SHADOW_EXTS = ['.d.ts', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
|
|
38
|
+
/**
|
|
39
|
+
* Enumerate pre-existing paths whose import-resolution `added` can steal.
|
|
40
|
+
*
|
|
41
|
+
* @param added — repo-relative path of a newly-added file
|
|
42
|
+
* @returns deduplicated list of candidate paths (NOT filtered against
|
|
43
|
+
* any known-files set — caller does that)
|
|
44
|
+
*/
|
|
45
|
+
export const shadowCandidatesFor = (added) => {
|
|
46
|
+
const ext = SHADOW_EXTS.find((e) => added.endsWith(e));
|
|
47
|
+
if (!ext)
|
|
48
|
+
return [];
|
|
49
|
+
const noExt = added.slice(0, -ext.length);
|
|
50
|
+
const out = new Set();
|
|
51
|
+
// (a) Same basename, different extension.
|
|
52
|
+
for (const alt of SHADOW_EXTS) {
|
|
53
|
+
if (alt !== ext)
|
|
54
|
+
out.add(noExt + alt);
|
|
55
|
+
}
|
|
56
|
+
// (b) Bare file beats sibling directory-style index.
|
|
57
|
+
for (const idx of SHADOW_EXTS) {
|
|
58
|
+
out.add(`${noExt}/index${idx}`);
|
|
59
|
+
out.add(`${noExt}\\index${idx}`);
|
|
60
|
+
}
|
|
61
|
+
// (c) New `foo/index.ext` shadows old `foo.ext`.
|
|
62
|
+
const idxSuffixSlash = '/index';
|
|
63
|
+
const idxSuffixBack = '\\index';
|
|
64
|
+
let dir = null;
|
|
65
|
+
if (noExt.endsWith(idxSuffixSlash))
|
|
66
|
+
dir = noExt.slice(0, -idxSuffixSlash.length);
|
|
67
|
+
else if (noExt.endsWith(idxSuffixBack))
|
|
68
|
+
dir = noExt.slice(0, -idxSuffixBack.length);
|
|
69
|
+
if (dir !== null) {
|
|
70
|
+
for (const alt of SHADOW_EXTS)
|
|
71
|
+
out.add(dir + alt);
|
|
72
|
+
}
|
|
73
|
+
return [...out];
|
|
74
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subgraph extraction for incremental DB writeback.
|
|
3
|
+
*
|
|
4
|
+
* Given the FULL ctx.graph produced by the pipeline (all files parsed,
|
|
5
|
+
* all phases run) and the set of file paths whose DB rows must be
|
|
6
|
+
* replaced, produce a smaller KnowledgeGraph that contains:
|
|
7
|
+
*
|
|
8
|
+
* - Every node whose `properties.filePath` is in `toWriteSet`.
|
|
9
|
+
* - Every graph-wide node (Community, Process) — these are regenerated
|
|
10
|
+
* each run by the communities/processes phases and must be fully
|
|
11
|
+
* rewritten.
|
|
12
|
+
* - Every relationship where AT LEAST ONE endpoint is in the writable
|
|
13
|
+
* set above. Relationships entirely between unchanged-file nodes
|
|
14
|
+
* are skipped — their rows are still in the DB and re-inserting
|
|
15
|
+
* them would PK-conflict at COPY time.
|
|
16
|
+
*
|
|
17
|
+
* The resulting subgraph is what gets passed to `loadGraphToLbug` after
|
|
18
|
+
* the orchestrator has deleted the corresponding DB rows. Hydrated
|
|
19
|
+
* unchanged-file rows are never touched in the DB.
|
|
20
|
+
*
|
|
21
|
+
* # Cross-file edge consistency (Finding 1)
|
|
22
|
+
*
|
|
23
|
+
* `extractChangedSubgraph` intentionally does NOT expand the set it is
|
|
24
|
+
* given — expansion is the orchestrator's job, so the SAME expanded set
|
|
25
|
+
* can be fed to both `deleteNodesForFile` and this function (asymmetry
|
|
26
|
+
* between the delete set and the write set silently corrupts the DB).
|
|
27
|
+
* `computeEffectiveWriteSet` below performs the boundary-crossing 1-hop
|
|
28
|
+
* walk; the orchestrator composes it with its importer-BFS expansion and
|
|
29
|
+
* passes the result here.
|
|
30
|
+
*
|
|
31
|
+
* Why the 1-hop walk is needed: consider a barrel re-export change —
|
|
32
|
+
* file C (a barrel) shifts `export { foo } from './b'` to
|
|
33
|
+
* `export { foo } from './d'`. After scope resolution, file A's CALLS
|
|
34
|
+
* edge to `foo` resolves to D instead of B, even though A's content is
|
|
35
|
+
* byte-for-byte identical:
|
|
36
|
+
*
|
|
37
|
+
* - Old A→B edge survives in DB (neither A nor B is changed → not deleted)
|
|
38
|
+
* - New A→D edge is missing (neither A nor D in writable set → skipped)
|
|
39
|
+
*
|
|
40
|
+
* Pulling the unchanged-side file of every writable-boundary-crossing
|
|
41
|
+
* edge into the write set fixes both halves: the orchestrator's
|
|
42
|
+
* `DETACH DELETE` cleans up the stale unchanged-side rows, and the new
|
|
43
|
+
* cross-file edges land because at least one endpoint is now writable.
|
|
44
|
+
*
|
|
45
|
+
* Limitation (documented): if a file X *stopped* importing from a
|
|
46
|
+
* changed file C, X has no edge to C in the new graph, so this 1-hop
|
|
47
|
+
* walk doesn't catch it. The orchestrator's importer-BFS (which reads
|
|
48
|
+
* IMPORTS from the pre-pipeline DB) covers that case instead.
|
|
49
|
+
*/
|
|
50
|
+
import type { KnowledgeGraph } from '../graph/types.js';
|
|
51
|
+
export declare const extractChangedSubgraph: (fullGraph: KnowledgeGraph, toWriteSet: ReadonlySet<string>) => KnowledgeGraph;
|
|
52
|
+
/**
|
|
53
|
+
* Public — derive the EFFECTIVE write-set: `toWriteSet` expanded by one
|
|
54
|
+
* hop along every edge in the new graph that crosses the writable
|
|
55
|
+
* boundary (one endpoint in a writable file, the other in an unchanged
|
|
56
|
+
* file). The unchanged-side file is pulled in so its stale rows are
|
|
57
|
+
* deleted + rewritten in lockstep with the changed side.
|
|
58
|
+
*
|
|
59
|
+
* Single pass over the edge list. Does NOT mutate `toWriteSet`. The
|
|
60
|
+
* orchestrator MUST feed the returned set to both `deleteNodesForFile`
|
|
61
|
+
* and `extractChangedSubgraph` — feeding the unexpanded set to either
|
|
62
|
+
* one leaves stale rows or PK-conflicts at COPY time.
|
|
63
|
+
*/
|
|
64
|
+
export declare const computeEffectiveWriteSet: (fullGraph: KnowledgeGraph, toWriteSet: ReadonlySet<string>) => Set<string>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subgraph extraction for incremental DB writeback.
|
|
3
|
+
*
|
|
4
|
+
* Given the FULL ctx.graph produced by the pipeline (all files parsed,
|
|
5
|
+
* all phases run) and the set of file paths whose DB rows must be
|
|
6
|
+
* replaced, produce a smaller KnowledgeGraph that contains:
|
|
7
|
+
*
|
|
8
|
+
* - Every node whose `properties.filePath` is in `toWriteSet`.
|
|
9
|
+
* - Every graph-wide node (Community, Process) — these are regenerated
|
|
10
|
+
* each run by the communities/processes phases and must be fully
|
|
11
|
+
* rewritten.
|
|
12
|
+
* - Every relationship where AT LEAST ONE endpoint is in the writable
|
|
13
|
+
* set above. Relationships entirely between unchanged-file nodes
|
|
14
|
+
* are skipped — their rows are still in the DB and re-inserting
|
|
15
|
+
* them would PK-conflict at COPY time.
|
|
16
|
+
*
|
|
17
|
+
* The resulting subgraph is what gets passed to `loadGraphToLbug` after
|
|
18
|
+
* the orchestrator has deleted the corresponding DB rows. Hydrated
|
|
19
|
+
* unchanged-file rows are never touched in the DB.
|
|
20
|
+
*
|
|
21
|
+
* # Cross-file edge consistency (Finding 1)
|
|
22
|
+
*
|
|
23
|
+
* `extractChangedSubgraph` intentionally does NOT expand the set it is
|
|
24
|
+
* given — expansion is the orchestrator's job, so the SAME expanded set
|
|
25
|
+
* can be fed to both `deleteNodesForFile` and this function (asymmetry
|
|
26
|
+
* between the delete set and the write set silently corrupts the DB).
|
|
27
|
+
* `computeEffectiveWriteSet` below performs the boundary-crossing 1-hop
|
|
28
|
+
* walk; the orchestrator composes it with its importer-BFS expansion and
|
|
29
|
+
* passes the result here.
|
|
30
|
+
*
|
|
31
|
+
* Why the 1-hop walk is needed: consider a barrel re-export change —
|
|
32
|
+
* file C (a barrel) shifts `export { foo } from './b'` to
|
|
33
|
+
* `export { foo } from './d'`. After scope resolution, file A's CALLS
|
|
34
|
+
* edge to `foo` resolves to D instead of B, even though A's content is
|
|
35
|
+
* byte-for-byte identical:
|
|
36
|
+
*
|
|
37
|
+
* - Old A→B edge survives in DB (neither A nor B is changed → not deleted)
|
|
38
|
+
* - New A→D edge is missing (neither A nor D in writable set → skipped)
|
|
39
|
+
*
|
|
40
|
+
* Pulling the unchanged-side file of every writable-boundary-crossing
|
|
41
|
+
* edge into the write set fixes both halves: the orchestrator's
|
|
42
|
+
* `DETACH DELETE` cleans up the stale unchanged-side rows, and the new
|
|
43
|
+
* cross-file edges land because at least one endpoint is now writable.
|
|
44
|
+
*
|
|
45
|
+
* Limitation (documented): if a file X *stopped* importing from a
|
|
46
|
+
* changed file C, X has no edge to C in the new graph, so this 1-hop
|
|
47
|
+
* walk doesn't catch it. The orchestrator's importer-BFS (which reads
|
|
48
|
+
* IMPORTS from the pre-pipeline DB) covers that case instead.
|
|
49
|
+
*/
|
|
50
|
+
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
51
|
+
const isGraphWide = (label) => label === 'Community' || label === 'Process';
|
|
52
|
+
/**
|
|
53
|
+
* Build a Map<nodeId, filePath> for every File-bound node in the graph.
|
|
54
|
+
* Graph-wide nodes (Community/Process) have no filePath and are filtered.
|
|
55
|
+
*/
|
|
56
|
+
const indexNodeFilePaths = (fullGraph) => {
|
|
57
|
+
const idx = new Map();
|
|
58
|
+
fullGraph.forEachNode((n) => {
|
|
59
|
+
const fp = n.properties?.filePath;
|
|
60
|
+
if (fp)
|
|
61
|
+
idx.set(n.id, fp);
|
|
62
|
+
});
|
|
63
|
+
return idx;
|
|
64
|
+
};
|
|
65
|
+
export const extractChangedSubgraph = (fullGraph, toWriteSet) => {
|
|
66
|
+
const sub = createKnowledgeGraph();
|
|
67
|
+
const writableNodeIds = new Set();
|
|
68
|
+
fullGraph.forEachNode((n) => {
|
|
69
|
+
const filePath = n.properties?.filePath;
|
|
70
|
+
const include = (filePath && toWriteSet.has(filePath)) || isGraphWide(n.label);
|
|
71
|
+
if (include) {
|
|
72
|
+
sub.addNode(n);
|
|
73
|
+
writableNodeIds.add(n.id);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
fullGraph.forEachRelationship((r) => {
|
|
77
|
+
if (writableNodeIds.has(r.sourceId) || writableNodeIds.has(r.targetId)) {
|
|
78
|
+
sub.addRelationship(r);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return sub;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Public — derive the EFFECTIVE write-set: `toWriteSet` expanded by one
|
|
85
|
+
* hop along every edge in the new graph that crosses the writable
|
|
86
|
+
* boundary (one endpoint in a writable file, the other in an unchanged
|
|
87
|
+
* file). The unchanged-side file is pulled in so its stale rows are
|
|
88
|
+
* deleted + rewritten in lockstep with the changed side.
|
|
89
|
+
*
|
|
90
|
+
* Single pass over the edge list. Does NOT mutate `toWriteSet`. The
|
|
91
|
+
* orchestrator MUST feed the returned set to both `deleteNodesForFile`
|
|
92
|
+
* and `extractChangedSubgraph` — feeding the unexpanded set to either
|
|
93
|
+
* one leaves stale rows or PK-conflicts at COPY time.
|
|
94
|
+
*/
|
|
95
|
+
export const computeEffectiveWriteSet = (fullGraph, toWriteSet) => {
|
|
96
|
+
const nodeFilePaths = indexNodeFilePaths(fullGraph);
|
|
97
|
+
const expanded = new Set(toWriteSet);
|
|
98
|
+
fullGraph.forEachRelationship((r) => {
|
|
99
|
+
const sourcePath = nodeFilePaths.get(r.sourceId);
|
|
100
|
+
const targetPath = nodeFilePaths.get(r.targetId);
|
|
101
|
+
if (!sourcePath || !targetPath)
|
|
102
|
+
return; // skip edges to graph-wide nodes
|
|
103
|
+
const sourceWritable = toWriteSet.has(sourcePath);
|
|
104
|
+
const targetWritable = toWriteSet.has(targetPath);
|
|
105
|
+
if (sourceWritable && !targetWritable)
|
|
106
|
+
expanded.add(targetPath);
|
|
107
|
+
else if (targetWritable && !sourceWritable)
|
|
108
|
+
expanded.add(sourcePath);
|
|
109
|
+
});
|
|
110
|
+
return expanded;
|
|
111
|
+
};
|
|
@@ -30,7 +30,7 @@ import { isRegistryPrimary } from './registry-primary-flag.js';
|
|
|
30
30
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
31
31
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
32
32
|
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
|
33
|
-
import {
|
|
33
|
+
import { CLASS_CONTAINER_TYPES, FUNCTION_NODE_TYPES, findEnclosingClassInfo, genericFuncName, inferFunctionLabel, } from './utils/ast-helpers.js';
|
|
34
34
|
import { typeTagForId, constTagForId, buildCollisionGroups } from './utils/method-props.js';
|
|
35
35
|
import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, extractCallArgTypes, } from './utils/call-analysis.js';
|
|
36
36
|
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
@@ -39,6 +39,57 @@ import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
|
39
39
|
import { extractTemplateComponents } from './vue-sfc-extractor.js';
|
|
40
40
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
41
41
|
import { logger } from '../logger.js';
|
|
42
|
+
// ── Property-prepass helpers (parity with parse-worker.ts) ──
|
|
43
|
+
// These mirror the sequential-path equivalents in parse-worker.ts so the main-
|
|
44
|
+
// thread `processCalls` pre-pass produces byte-identical Property nodes/symbols
|
|
45
|
+
// to the worker pool. Drift between the two paths breaks the
|
|
46
|
+
// `incremental ≡ --force` invariant the moment a repo crosses the worker
|
|
47
|
+
// threshold between runs.
|
|
48
|
+
/** Walk up to the nearest enclosing class/struct/interface AST node. */
|
|
49
|
+
const findEnclosingClassNode = (node) => {
|
|
50
|
+
let current = node.parent;
|
|
51
|
+
while (current) {
|
|
52
|
+
if (CLASS_CONTAINER_TYPES.has(current.type))
|
|
53
|
+
return current;
|
|
54
|
+
current = current.parent;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
/** No-op SymbolTable stub for FieldExtractorContext — matches parse-worker. */
|
|
59
|
+
const NOOP_SYMBOL_TABLE = {
|
|
60
|
+
lookupExact: () => undefined,
|
|
61
|
+
lookupExactFull: () => undefined,
|
|
62
|
+
lookupExactAll: () => [],
|
|
63
|
+
lookupCallableByName: () => [],
|
|
64
|
+
getFiles: () => [][Symbol.iterator](),
|
|
65
|
+
getStats: () => ({ fileCount: 0 }),
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Extract (and cache) field info for a class node. Cache is passed in so it
|
|
69
|
+
* stays scoped to a single `processCalls` invocation rather than leaking
|
|
70
|
+
* across analyze runs (worker uses module-level caching because each worker
|
|
71
|
+
* process is short-lived; the main thread is not).
|
|
72
|
+
*
|
|
73
|
+
* Cache key is `${filePath}:${classNode.startIndex}` — startIndex alone is a
|
|
74
|
+
* per-file byte offset, so almost every Ruby/Python file's leading class lands
|
|
75
|
+
* at byte 0 and would collide across files in the shared map.
|
|
76
|
+
*/
|
|
77
|
+
const getFieldInfo = (classNode, provider, context, cache) => {
|
|
78
|
+
if (!provider.fieldExtractor)
|
|
79
|
+
return undefined;
|
|
80
|
+
const cacheKey = `${context.filePath}:${classNode.startIndex}`;
|
|
81
|
+
const cached = cache.get(cacheKey);
|
|
82
|
+
if (cached)
|
|
83
|
+
return cached;
|
|
84
|
+
const result = provider.fieldExtractor.extract(classNode, context);
|
|
85
|
+
if (!result?.fields?.length)
|
|
86
|
+
return undefined;
|
|
87
|
+
const map = new Map();
|
|
88
|
+
for (const field of result.fields)
|
|
89
|
+
map.set(field.name, field);
|
|
90
|
+
cache.set(cacheKey, map);
|
|
91
|
+
return map;
|
|
92
|
+
};
|
|
42
93
|
/**
|
|
43
94
|
* Type labels treated as class-like **method-dispatch receivers** by the call
|
|
44
95
|
* resolver — the set walked by the MRO / heritage path for member and static
|
|
@@ -702,6 +753,111 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
702
753
|
}
|
|
703
754
|
prepared.push({ file, language, provider, tree, matches, parentMap, typeEnv });
|
|
704
755
|
}
|
|
756
|
+
// ── Property-registration pre-pass ──
|
|
757
|
+
// Register all routed properties (e.g. Ruby attr_accessor) BEFORE the
|
|
758
|
+
// resolution loop so cross-file field-type lookups (e.g.
|
|
759
|
+
// `user.address.save → Address#save`) succeed regardless of file
|
|
760
|
+
// processing order. This MUST stay in lockstep with the equivalent
|
|
761
|
+
// worker-path block in parse-worker.ts (kind === 'properties') — any
|
|
762
|
+
// divergence between the two paths breaks the `incremental ≡ --force`
|
|
763
|
+
// invariant once a repo crosses the worker threshold between runs.
|
|
764
|
+
const fieldInfoCache = new Map();
|
|
765
|
+
for (const { file, language, provider, matches, typeEnv } of prepared) {
|
|
766
|
+
const callRouter = provider.callRouter;
|
|
767
|
+
if (!callRouter)
|
|
768
|
+
continue;
|
|
769
|
+
matches.forEach((match) => {
|
|
770
|
+
const captureMap = {};
|
|
771
|
+
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
772
|
+
if (!captureMap['call'])
|
|
773
|
+
return;
|
|
774
|
+
const callNameNode = captureMap['call.name'];
|
|
775
|
+
if (!callNameNode)
|
|
776
|
+
return;
|
|
777
|
+
const routed = callRouter(callNameNode.text, captureMap['call']);
|
|
778
|
+
if (!routed || routed.kind !== 'properties')
|
|
779
|
+
return;
|
|
780
|
+
const propEnclosingInfo = findEnclosingClassInfo(captureMap['call'], file.path, provider.resolveEnclosingOwner);
|
|
781
|
+
const propEnclosingClassId = propEnclosingInfo?.classId ?? null;
|
|
782
|
+
// Enrich routed properties with FieldExtractor metadata so types
|
|
783
|
+
// discovered from constructor assignments (e.g. `@address = Address.new`)
|
|
784
|
+
// are propagated even when the routing payload itself lacks declaredType.
|
|
785
|
+
let routedFieldMap;
|
|
786
|
+
if (provider.fieldExtractor && typeEnv) {
|
|
787
|
+
const classNode = findEnclosingClassNode(captureMap['call']);
|
|
788
|
+
if (classNode) {
|
|
789
|
+
routedFieldMap = getFieldInfo(classNode, provider, {
|
|
790
|
+
typeEnv,
|
|
791
|
+
symbolTable: NOOP_SYMBOL_TABLE,
|
|
792
|
+
filePath: file.path,
|
|
793
|
+
language,
|
|
794
|
+
}, fieldInfoCache);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const fileId = generateId('File', file.path);
|
|
798
|
+
for (const item of routed.items) {
|
|
799
|
+
const routedFieldInfo = routedFieldMap?.get(item.propName);
|
|
800
|
+
const propQualifiedName = propEnclosingInfo
|
|
801
|
+
? `${propEnclosingInfo.className}.${item.propName}`
|
|
802
|
+
: item.propName;
|
|
803
|
+
const nodeId = generateId('Property', `${file.path}:${propQualifiedName}`);
|
|
804
|
+
graph.addNode({
|
|
805
|
+
id: nodeId,
|
|
806
|
+
label: 'Property',
|
|
807
|
+
properties: {
|
|
808
|
+
name: item.propName,
|
|
809
|
+
filePath: file.path,
|
|
810
|
+
startLine: item.startLine,
|
|
811
|
+
endLine: item.endLine,
|
|
812
|
+
language,
|
|
813
|
+
isExported: true,
|
|
814
|
+
description: item.accessorType,
|
|
815
|
+
...(item.declaredType
|
|
816
|
+
? { declaredType: item.declaredType }
|
|
817
|
+
: routedFieldInfo?.type
|
|
818
|
+
? { declaredType: routedFieldInfo.type }
|
|
819
|
+
: {}),
|
|
820
|
+
...(routedFieldInfo?.visibility !== undefined
|
|
821
|
+
? { visibility: routedFieldInfo.visibility }
|
|
822
|
+
: {}),
|
|
823
|
+
...(routedFieldInfo?.isStatic !== undefined
|
|
824
|
+
? { isStatic: routedFieldInfo.isStatic }
|
|
825
|
+
: {}),
|
|
826
|
+
...(routedFieldInfo?.isReadonly !== undefined
|
|
827
|
+
? { isReadonly: routedFieldInfo.isReadonly }
|
|
828
|
+
: {}),
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
ctx.model.symbols.add(file.path, item.propName, nodeId, 'Property', {
|
|
832
|
+
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
|
833
|
+
...(item.declaredType
|
|
834
|
+
? { declaredType: item.declaredType }
|
|
835
|
+
: routedFieldInfo?.type
|
|
836
|
+
? { declaredType: routedFieldInfo.type }
|
|
837
|
+
: {}),
|
|
838
|
+
});
|
|
839
|
+
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
840
|
+
graph.addRelationship({
|
|
841
|
+
id: relId,
|
|
842
|
+
sourceId: fileId,
|
|
843
|
+
targetId: nodeId,
|
|
844
|
+
type: 'DEFINES',
|
|
845
|
+
confidence: 1.0,
|
|
846
|
+
reason: '',
|
|
847
|
+
});
|
|
848
|
+
if (propEnclosingClassId) {
|
|
849
|
+
graph.addRelationship({
|
|
850
|
+
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
|
851
|
+
sourceId: propEnclosingClassId,
|
|
852
|
+
targetId: nodeId,
|
|
853
|
+
type: 'HAS_PROPERTY',
|
|
854
|
+
confidence: 1.0,
|
|
855
|
+
reason: '',
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
705
861
|
// ── Resolution loop: verify constructor bindings and resolve calls ──
|
|
706
862
|
// The accumulator (if present) is now fully populated from the preparation
|
|
707
863
|
// loop above, so verifyConstructorBindings sees all provider bindings
|
|
@@ -750,9 +906,10 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
750
906
|
if (receiverTypeName) {
|
|
751
907
|
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
|
|
752
908
|
const srcId = enclosing || generateId('File', file.path);
|
|
753
|
-
// Defer resolution
|
|
754
|
-
//
|
|
755
|
-
//
|
|
909
|
+
// Defer resolution so write-access tracking sees the FINAL graph
|
|
910
|
+
// state — properties from the pre-pass are present, but receiver-type
|
|
911
|
+
// resolution can still depend on inference that completes during the
|
|
912
|
+
// main loop. Resolve after all files have been processed.
|
|
756
913
|
pendingWrites.push({ receiverTypeName, propertyName, filePath: file.path, srcId });
|
|
757
914
|
}
|
|
758
915
|
// Assignment-only capture (no @call sibling): skip the rest of this
|
|
@@ -842,47 +999,8 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
842
999
|
case 'import':
|
|
843
1000
|
return;
|
|
844
1001
|
case 'properties': {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
for (const item of routed.items) {
|
|
848
|
-
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
|
849
|
-
graph.addNode({
|
|
850
|
-
id: nodeId,
|
|
851
|
-
label: 'Property',
|
|
852
|
-
properties: {
|
|
853
|
-
name: item.propName,
|
|
854
|
-
filePath: file.path,
|
|
855
|
-
startLine: item.startLine,
|
|
856
|
-
endLine: item.endLine,
|
|
857
|
-
language,
|
|
858
|
-
isExported: true,
|
|
859
|
-
description: item.accessorType,
|
|
860
|
-
},
|
|
861
|
-
});
|
|
862
|
-
ctx.model.symbols.add(file.path, item.propName, nodeId, 'Property', {
|
|
863
|
-
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
|
864
|
-
...(item.declaredType ? { declaredType: item.declaredType } : {}),
|
|
865
|
-
});
|
|
866
|
-
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
867
|
-
graph.addRelationship({
|
|
868
|
-
id: relId,
|
|
869
|
-
sourceId: fileId,
|
|
870
|
-
targetId: nodeId,
|
|
871
|
-
type: 'DEFINES',
|
|
872
|
-
confidence: 1.0,
|
|
873
|
-
reason: '',
|
|
874
|
-
});
|
|
875
|
-
if (propEnclosingClassId) {
|
|
876
|
-
graph.addRelationship({
|
|
877
|
-
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
|
878
|
-
sourceId: propEnclosingClassId,
|
|
879
|
-
targetId: nodeId,
|
|
880
|
-
type: 'HAS_PROPERTY',
|
|
881
|
-
confidence: 1.0,
|
|
882
|
-
reason: '',
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
}
|
|
1002
|
+
// Properties already registered in the pre-pass above.
|
|
1003
|
+
// Skip to avoid duplicate nodes/edges.
|
|
886
1004
|
return;
|
|
887
1005
|
}
|
|
888
1006
|
case 'call':
|
|
@@ -20,6 +20,23 @@ const __dirname = dirname(__filename);
|
|
|
20
20
|
const leidenPath = resolve(__dirname, '..', '..', '..', 'vendor', 'leiden', 'index.cjs');
|
|
21
21
|
const _require = createRequire(import.meta.url);
|
|
22
22
|
const leiden = _require(leidenPath);
|
|
23
|
+
/**
|
|
24
|
+
* Deterministic PRNG (mulberry32) seed for the vendored Leiden algorithm.
|
|
25
|
+
* Vendored Leiden defaults `rng: Math.random`, which makes community
|
|
26
|
+
* assignment non-deterministic across runs. Passing a seeded RNG gives us
|
|
27
|
+
* reproducible community/modularity output, which is required for the
|
|
28
|
+
* incremental-indexing equivalence test (incremental ≡ full rebuild).
|
|
29
|
+
*/
|
|
30
|
+
const LEIDEN_SEED = 0xc0de;
|
|
31
|
+
function createSeededRng(seed) {
|
|
32
|
+
let s = seed >>> 0;
|
|
33
|
+
return () => {
|
|
34
|
+
s = (s + 0x6d2b79f5) >>> 0;
|
|
35
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
36
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
37
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
23
40
|
// ============================================================================
|
|
24
41
|
// COMMUNITY COLORS (for visualization)
|
|
25
42
|
// ============================================================================
|
|
@@ -83,6 +100,7 @@ export const processCommunities = async (knowledgeGraph, onProgress) => {
|
|
|
83
100
|
Promise.resolve(leiden.detailed(graph, {
|
|
84
101
|
resolution: isLarge ? 2.0 : 1.0,
|
|
85
102
|
maxIterations: isLarge ? 3 : 0,
|
|
103
|
+
rng: createSeededRng(LEIDEN_SEED),
|
|
86
104
|
})),
|
|
87
105
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Leiden timeout')), LEIDEN_TIMEOUT_MS)),
|
|
88
106
|
]);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract Java arity metadata from a method-like tree-sitter node —
|
|
3
|
+
* `method_declaration` or `constructor_declaration`.
|
|
4
|
+
*
|
|
5
|
+
* Reuses `javaMethodConfig.extractParameters` so scope-extracted defs
|
|
6
|
+
* carry the same arity semantics as the legacy parse-worker path:
|
|
7
|
+
* - varargs (`...`) collapses `parameterCount` to `undefined`
|
|
8
|
+
* - `parameterTypes` collects declared type names; a literal
|
|
9
|
+
* `'varargs'` marker is appended for variadic methods so
|
|
10
|
+
* `javaArityCompatibility` can detect them.
|
|
11
|
+
*/
|
|
12
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
13
|
+
export interface JavaArityMetadata {
|
|
14
|
+
readonly parameterCount: number | undefined;
|
|
15
|
+
readonly requiredParameterCount: number | undefined;
|
|
16
|
+
readonly parameterTypes: readonly string[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function computeJavaArityMetadata(fnNode: SyntaxNode): JavaArityMetadata;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract Java arity metadata from a method-like tree-sitter node —
|
|
3
|
+
* `method_declaration` or `constructor_declaration`.
|
|
4
|
+
*
|
|
5
|
+
* Reuses `javaMethodConfig.extractParameters` so scope-extracted defs
|
|
6
|
+
* carry the same arity semantics as the legacy parse-worker path:
|
|
7
|
+
* - varargs (`...`) collapses `parameterCount` to `undefined`
|
|
8
|
+
* - `parameterTypes` collects declared type names; a literal
|
|
9
|
+
* `'varargs'` marker is appended for variadic methods so
|
|
10
|
+
* `javaArityCompatibility` can detect them.
|
|
11
|
+
*/
|
|
12
|
+
import { javaMethodConfig } from '../../method-extractors/configs/jvm.js';
|
|
13
|
+
export function computeJavaArityMetadata(fnNode) {
|
|
14
|
+
const params = javaMethodConfig.extractParameters?.(fnNode) ?? [];
|
|
15
|
+
let hasVariadic = false;
|
|
16
|
+
const types = [];
|
|
17
|
+
for (const p of params) {
|
|
18
|
+
if (p.isVariadic)
|
|
19
|
+
hasVariadic = true;
|
|
20
|
+
if (p.type !== null)
|
|
21
|
+
types.push(p.type);
|
|
22
|
+
}
|
|
23
|
+
if (hasVariadic)
|
|
24
|
+
types.push('varargs');
|
|
25
|
+
const total = params.length;
|
|
26
|
+
// For varargs methods, `parameterCount` (max) is unknown — any number of
|
|
27
|
+
// trailing arguments is valid. But the fixed-prefix parameters (everything
|
|
28
|
+
// before the variadic `...` param) are still required, so we preserve that
|
|
29
|
+
// count in `requiredParameterCount` so `javaArityCompatibility` can reject
|
|
30
|
+
// calls that undersupply the fixed prefix (e.g. `f(int x, String... args)`
|
|
31
|
+
// called with 0 args).
|
|
32
|
+
const fixedCount = params.filter((p) => !p.isVariadic).length;
|
|
33
|
+
const parameterCount = hasVariadic ? undefined : total;
|
|
34
|
+
const requiredParameterCount = hasVariadic ? fixedCount : total;
|
|
35
|
+
return {
|
|
36
|
+
parameterCount,
|
|
37
|
+
requiredParameterCount,
|
|
38
|
+
parameterTypes: types.length > 0 ? types : undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Java arity check, accommodating varargs (`...`).
|
|
3
|
+
*
|
|
4
|
+
* Verdicts:
|
|
5
|
+
* - `'compatible'` — argCount matches parameterCount, OR varargs present.
|
|
6
|
+
* - `'incompatible'` — argCount mismatches with no varargs.
|
|
7
|
+
* - `'unknown'` — metadata absent / incomplete.
|
|
8
|
+
*/
|
|
9
|
+
import type { Callsite, SymbolDefinition } from '../../../../_shared/index.js';
|
|
10
|
+
export declare function javaArityCompatibility(def: SymbolDefinition, callsite: Callsite): 'compatible' | 'unknown' | 'incompatible';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Java arity check, accommodating varargs (`...`).
|
|
3
|
+
*
|
|
4
|
+
* Verdicts:
|
|
5
|
+
* - `'compatible'` — argCount matches parameterCount, OR varargs present.
|
|
6
|
+
* - `'incompatible'` — argCount mismatches with no varargs.
|
|
7
|
+
* - `'unknown'` — metadata absent / incomplete.
|
|
8
|
+
*/
|
|
9
|
+
export function javaArityCompatibility(def, callsite) {
|
|
10
|
+
const max = def.parameterCount;
|
|
11
|
+
const min = def.requiredParameterCount;
|
|
12
|
+
if (max === undefined && min === undefined)
|
|
13
|
+
return 'unknown';
|
|
14
|
+
const argCount = callsite.arity;
|
|
15
|
+
if (!Number.isFinite(argCount) || argCount < 0)
|
|
16
|
+
return 'unknown';
|
|
17
|
+
const hasVarArgs = def.parameterTypes !== undefined &&
|
|
18
|
+
def.parameterTypes.some((t) => t === 'varargs' || t.includes('...'));
|
|
19
|
+
if (min !== undefined && argCount < min)
|
|
20
|
+
return 'incompatible';
|
|
21
|
+
if (max !== undefined && argCount > max && !hasVarArgs)
|
|
22
|
+
return 'incompatible';
|
|
23
|
+
return 'compatible';
|
|
24
|
+
}
|