gitnexus 1.6.11-rc.28 → 1.6.11-rc.29
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/cli/analyze.js +2 -0
- package/dist/core/analyzer-identity.js +21 -3
- package/dist/core/ingestion/model/field-registry.d.ts +3 -3
- package/dist/core/ingestion/model/field-registry.js +11 -7
- package/dist/core/ingestion/model/method-registry.d.ts +3 -3
- package/dist/core/ingestion/model/method-registry.js +13 -8
- package/dist/core/ingestion/model/type-registry.js +8 -4
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +28 -0
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.d.ts +11 -1
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.js +26 -6
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +5 -2
- package/dist/core/lbug/csv-generator.js +22 -20
- package/package.json +1 -1
package/dist/cli/analyze.js
CHANGED
|
@@ -524,6 +524,8 @@ const ANALYZE_CLI_ENV_KEYS = [
|
|
|
524
524
|
'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE',
|
|
525
525
|
'GITNEXUS_EMBEDDING_DEVICE',
|
|
526
526
|
'GITNEXUS_ANALYZE_PROGRESS_ACTIVE',
|
|
527
|
+
'GITNEXUS_ANALYZER_IDENTITY_IN_PROCESS_GUARDS',
|
|
528
|
+
'GITNEXUS_RESOLVE_DEF_GRAPH_ID_MEMO',
|
|
527
529
|
'GITNEXUS_EMBEDDING_URL',
|
|
528
530
|
'GITNEXUS_EMBEDDING_MODEL',
|
|
529
531
|
'GITNEXUS_EMBEDDING_API_KEY',
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* different entry files inside the same build tree; alternating between them
|
|
14
14
|
* must not make an otherwise identical index stale.
|
|
15
15
|
*/
|
|
16
|
-
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
16
|
+
import { accessSync, closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
17
17
|
import { createHash, randomBytes } from 'node:crypto';
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
19
|
import { isDeepStrictEqual } from 'node:util';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { parseTruthyEnv } from './ingestion/utils/env.js';
|
|
22
23
|
import { fileURLToPath } from 'node:url';
|
|
23
24
|
export const ANALYZER_RUNNER_IDENTITY_SCHEMA_VERSION = 4;
|
|
24
25
|
const BUILD_CANONICALIZATION = 'gitnexus-analyzer-build-v2';
|
|
@@ -1754,9 +1755,26 @@ function snapshotCacheGuardDirect(request) {
|
|
|
1754
1755
|
return null;
|
|
1755
1756
|
}
|
|
1756
1757
|
}
|
|
1757
|
-
function
|
|
1758
|
+
function installTreeUnwritable(packageRoot, buildRoot) {
|
|
1759
|
+
for (const dir of [packageRoot, buildRoot]) {
|
|
1760
|
+
try {
|
|
1761
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1762
|
+
}
|
|
1763
|
+
catch (err) {
|
|
1764
|
+
const code = err.code;
|
|
1765
|
+
if (code === 'EACCES' || code === 'EROFS')
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
function snapshotCacheGuards(requests, packageRoot, buildRoot) {
|
|
1758
1772
|
if (requests.length < 128)
|
|
1759
1773
|
return requests.map(snapshotCacheGuardDirect);
|
|
1774
|
+
if (parseTruthyEnv(process.env.GITNEXUS_ANALYZER_IDENTITY_IN_PROCESS_GUARDS) ||
|
|
1775
|
+
installTreeUnwritable(packageRoot, buildRoot)) {
|
|
1776
|
+
return requests.map(snapshotCacheGuardDirect);
|
|
1777
|
+
}
|
|
1760
1778
|
try {
|
|
1761
1779
|
const probe = spawnSync(process.execPath, ['--input-type=commonjs', '-e', CACHE_GUARD_PROBE_SCRIPT], {
|
|
1762
1780
|
input: JSON.stringify(requests),
|
|
@@ -1857,7 +1875,7 @@ function validateIdentityCache(cache, options) {
|
|
|
1857
1875
|
return { mode, absolutePath };
|
|
1858
1876
|
});
|
|
1859
1877
|
options.onCacheValidationPass?.({ guardCount: requests.length });
|
|
1860
|
-
const actual = snapshotCacheGuards(requests);
|
|
1878
|
+
const actual = snapshotCacheGuards(requests, cache.packageRoot, cache.buildRoot);
|
|
1861
1879
|
const mismatch = actual.findIndex((result, index) => !isDeepStrictEqual(result, entries[index][1]));
|
|
1862
1880
|
if (mismatch !== -1) {
|
|
1863
1881
|
const [mode, absolutePath] = JSON.parse(entries[mismatch][0]);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Field Registry
|
|
3
3
|
*
|
|
4
4
|
* Owner-scoped field/property index extracted from SymbolTable.
|
|
5
|
-
* Stores Property / Variable / Const / Static symbols
|
|
6
|
-
* `ownerNodeId
|
|
7
|
-
* under the same (owner, name) — e.g. legacy Property plus a
|
|
5
|
+
* Stores Property / Variable / Const / Static symbols in a nested
|
|
6
|
+
* `Map<ownerNodeId, Map<fieldName, defs[]>>` for O(1) lookup. Supports
|
|
7
|
+
* multiple defs under the same (owner, name) — e.g. legacy Property plus a
|
|
8
8
|
* scope-resolution Variable reconciliation entry.
|
|
9
9
|
*/
|
|
10
10
|
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Field Registry
|
|
3
3
|
*
|
|
4
4
|
* Owner-scoped field/property index extracted from SymbolTable.
|
|
5
|
-
* Stores Property / Variable / Const / Static symbols
|
|
6
|
-
* `ownerNodeId
|
|
7
|
-
* under the same (owner, name) — e.g. legacy Property plus a
|
|
5
|
+
* Stores Property / Variable / Const / Static symbols in a nested
|
|
6
|
+
* `Map<ownerNodeId, Map<fieldName, defs[]>>` for O(1) lookup. Supports
|
|
7
|
+
* multiple defs under the same (owner, name) — e.g. legacy Property plus a
|
|
8
8
|
* scope-resolution Variable reconciliation entry.
|
|
9
9
|
*/
|
|
10
10
|
const EMPTY = Object.freeze([]);
|
|
@@ -14,20 +14,24 @@ const EMPTY = Object.freeze([]);
|
|
|
14
14
|
export const createFieldRegistry = () => {
|
|
15
15
|
const fieldByOwner = new Map();
|
|
16
16
|
const lookupAllByOwner = (ownerNodeId, fieldName) => {
|
|
17
|
-
return fieldByOwner.get(
|
|
17
|
+
return fieldByOwner.get(ownerNodeId)?.get(fieldName) ?? EMPTY;
|
|
18
18
|
};
|
|
19
19
|
const lookupFieldByOwner = (ownerNodeId, fieldName) => {
|
|
20
20
|
const pool = lookupAllByOwner(ownerNodeId, fieldName);
|
|
21
21
|
return pool.length === 0 ? undefined : pool[0];
|
|
22
22
|
};
|
|
23
23
|
const register = (ownerNodeId, fieldName, def) => {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
let byName = fieldByOwner.get(ownerNodeId);
|
|
25
|
+
if (!byName) {
|
|
26
|
+
byName = new Map();
|
|
27
|
+
fieldByOwner.set(ownerNodeId, byName);
|
|
28
|
+
}
|
|
29
|
+
const existing = byName.get(fieldName);
|
|
26
30
|
if (existing) {
|
|
27
31
|
existing.push(def);
|
|
28
32
|
}
|
|
29
33
|
else {
|
|
30
|
-
|
|
34
|
+
byName.set(fieldName, [def]);
|
|
31
35
|
}
|
|
32
36
|
};
|
|
33
37
|
const clear = () => {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Method Registry
|
|
3
3
|
*
|
|
4
4
|
* Owner-scoped method index extracted from SymbolTable.
|
|
5
|
-
* Stores Method/Constructor/Function-with-ownerId symbols
|
|
6
|
-
* `ownerNodeId
|
|
7
|
-
* (array values) and arity-based filtering.
|
|
5
|
+
* Stores Method/Constructor/Function-with-ownerId symbols in a nested
|
|
6
|
+
* `Map<ownerNodeId, Map<methodName, defs[]>>` for O(1) lookup. Supports
|
|
7
|
+
* overloads (array values) and arity-based filtering.
|
|
8
8
|
*/
|
|
9
9
|
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
10
10
|
export interface MethodRegistry {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Method Registry
|
|
3
3
|
*
|
|
4
4
|
* Owner-scoped method index extracted from SymbolTable.
|
|
5
|
-
* Stores Method/Constructor/Function-with-ownerId symbols
|
|
6
|
-
* `ownerNodeId
|
|
7
|
-
* (array values) and arity-based filtering.
|
|
5
|
+
* Stores Method/Constructor/Function-with-ownerId symbols in a nested
|
|
6
|
+
* `Map<ownerNodeId, Map<methodName, defs[]>>` for O(1) lookup. Supports
|
|
7
|
+
* overloads (array values) and arity-based filtering.
|
|
8
8
|
*/
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Factory
|
|
@@ -19,8 +19,9 @@ export const createMethodRegistry = () => {
|
|
|
19
19
|
// Set once when a Function+ownerId def lands here, powers the Tier 3
|
|
20
20
|
// dedup fast-path. Monotonic: never unset except on `clear()`.
|
|
21
21
|
let hasFunctionMethodsFlag = false;
|
|
22
|
+
const ownerDefs = (ownerNodeId, methodName) => methodByOwner.get(ownerNodeId)?.get(methodName);
|
|
22
23
|
const lookupMethodByOwner = (ownerNodeId, methodName, argCount) => {
|
|
23
|
-
const defs =
|
|
24
|
+
const defs = ownerDefs(ownerNodeId, methodName);
|
|
24
25
|
if (!defs || defs.length === 0)
|
|
25
26
|
return undefined;
|
|
26
27
|
// Arity narrowing: when an argCount is provided and there are multiple
|
|
@@ -89,16 +90,20 @@ export const createMethodRegistry = () => {
|
|
|
89
90
|
return methodsByName.get(name) ?? EMPTY;
|
|
90
91
|
};
|
|
91
92
|
const lookupAllByOwner = (ownerNodeId, methodName) => {
|
|
92
|
-
return
|
|
93
|
+
return ownerDefs(ownerNodeId, methodName) ?? EMPTY;
|
|
93
94
|
};
|
|
94
95
|
const register = (ownerNodeId, methodName, def) => {
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
let owned = methodByOwner.get(ownerNodeId);
|
|
97
|
+
if (!owned) {
|
|
98
|
+
owned = new Map();
|
|
99
|
+
methodByOwner.set(ownerNodeId, owned);
|
|
100
|
+
}
|
|
101
|
+
const existing = owned.get(methodName);
|
|
97
102
|
if (existing) {
|
|
98
103
|
existing.push(def);
|
|
99
104
|
}
|
|
100
105
|
else {
|
|
101
|
-
|
|
106
|
+
owned.set(methodName, [def]);
|
|
102
107
|
}
|
|
103
108
|
const byName = methodsByName.get(methodName);
|
|
104
109
|
if (byName) {
|
|
@@ -24,7 +24,7 @@ export const createTypeRegistry = () => {
|
|
|
24
24
|
return implByName.get(name) ?? [];
|
|
25
25
|
};
|
|
26
26
|
const lookupAllByOwner = (ownerNodeId, simpleName) => {
|
|
27
|
-
return nestedByOwner.get(
|
|
27
|
+
return nestedByOwner.get(ownerNodeId)?.get(simpleName) ?? EMPTY;
|
|
28
28
|
};
|
|
29
29
|
const registerClass = (name, qualifiedName, def) => {
|
|
30
30
|
const existing = classByName.get(name);
|
|
@@ -52,13 +52,17 @@ export const createTypeRegistry = () => {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
const registerByOwner = (ownerNodeId, simpleName, def) => {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let byName = nestedByOwner.get(ownerNodeId);
|
|
56
|
+
if (!byName) {
|
|
57
|
+
byName = new Map();
|
|
58
|
+
nestedByOwner.set(ownerNodeId, byName);
|
|
59
|
+
}
|
|
60
|
+
const existing = byName.get(simpleName);
|
|
57
61
|
if (existing) {
|
|
58
62
|
existing.push(def);
|
|
59
63
|
}
|
|
60
64
|
else {
|
|
61
|
-
|
|
65
|
+
byName.set(simpleName, [def]);
|
|
62
66
|
}
|
|
63
67
|
};
|
|
64
68
|
const clear = () => {
|
|
@@ -22,6 +22,15 @@ import { isOverloadableCallable, isPositionQualifiedLocalLabel, } from '../../ut
|
|
|
22
22
|
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
23
23
|
import { parameterShapeIdTag } from '../../utils/method-props.js';
|
|
24
24
|
import { definitionIdPosition } from '../utils/definition-id.js';
|
|
25
|
+
const defGraphIdMemoByLookup = new WeakMap();
|
|
26
|
+
const isResolveDefGraphIdMemoEnabled = () => {
|
|
27
|
+
const raw = process.env.GITNEXUS_RESOLVE_DEF_GRAPH_ID_MEMO;
|
|
28
|
+
if (raw === undefined || raw.trim() === '')
|
|
29
|
+
return true;
|
|
30
|
+
const value = raw.trim().toLowerCase();
|
|
31
|
+
return value !== '0' && value !== 'false' && value !== 'off' && value !== 'no';
|
|
32
|
+
};
|
|
33
|
+
const defGraphIdMemoKey = (filePath, def) => `${filePath}\0${def.nodeId ?? ''}\0${def.type ?? ''}\0${def.qualifiedName ?? ''}\0${def.parameterCount ?? ''}\0${(def.parameterTypes ?? []).join(',')}\0${(def.parameterTypeClasses ?? []).join(',')}\0${def.namespacePrefix ?? ''}\0${(def.templateArguments ?? []).join(',')}\0${templateConstraintsIdTag(def.templateConstraints)}`;
|
|
25
34
|
/**
|
|
26
35
|
* Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
|
|
27
36
|
* source ("caller"). A Variable / Property can be the TARGET of an
|
|
@@ -188,6 +197,25 @@ function siblingCallableLabel(label) {
|
|
|
188
197
|
return undefined;
|
|
189
198
|
}
|
|
190
199
|
export function resolveDefGraphId(filePath, def, nodeLookup) {
|
|
200
|
+
if (!isResolveDefGraphIdMemoEnabled()) {
|
|
201
|
+
return resolveDefGraphIdUncached(filePath, def, nodeLookup);
|
|
202
|
+
}
|
|
203
|
+
const qn = def.qualifiedName;
|
|
204
|
+
if (qn === undefined || qn.length === 0)
|
|
205
|
+
return undefined;
|
|
206
|
+
let bucket = defGraphIdMemoByLookup.get(nodeLookup);
|
|
207
|
+
if (bucket === undefined) {
|
|
208
|
+
bucket = new Map();
|
|
209
|
+
defGraphIdMemoByLookup.set(nodeLookup, bucket);
|
|
210
|
+
}
|
|
211
|
+
const key = defGraphIdMemoKey(filePath, def);
|
|
212
|
+
if (bucket.has(key))
|
|
213
|
+
return bucket.get(key);
|
|
214
|
+
const resolved = resolveDefGraphIdUncached(filePath, def, nodeLookup);
|
|
215
|
+
bucket.set(key, resolved);
|
|
216
|
+
return resolved;
|
|
217
|
+
}
|
|
218
|
+
function resolveDefGraphIdUncached(filePath, def, nodeLookup) {
|
|
191
219
|
const qn = def.qualifiedName;
|
|
192
220
|
if (qn === undefined || qn.length === 0)
|
|
193
221
|
return undefined;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* values. Providers own syntax recognition; this pass consumes only the
|
|
4
4
|
* JSON-safe facts carried by ParsedFile.
|
|
5
5
|
*/
|
|
6
|
-
import type { ParsedFile, SymbolDefinition } from '../../../../_shared/index.js';
|
|
6
|
+
import type { CallableFlowExpectedSignature, ParsedFile, SymbolDefinition } from '../../../../_shared/index.js';
|
|
7
7
|
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
8
8
|
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
9
9
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
@@ -44,6 +44,14 @@ export interface EmitCallableValueFlowInput {
|
|
|
44
44
|
readonly isCallableValueTarget?: (def: SymbolDefinition) => boolean;
|
|
45
45
|
readonly hasFileLocalCallableLinkage?: (def: SymbolDefinition) => boolean;
|
|
46
46
|
readonly onWarn?: (warning: CallableValueFlowWarning) => void;
|
|
47
|
+
/** When set, skip a second `collectDeferredIndirectSites` walk. */
|
|
48
|
+
readonly deferredIndirectSites?: ReadonlySet<string>;
|
|
49
|
+
/** When set, skip a second `referenceSites` signature walk. */
|
|
50
|
+
readonly callSignaturesBySite?: ReadonlyMap<string, CallableFlowExpectedSignature>;
|
|
51
|
+
}
|
|
52
|
+
export interface DeferredIndirectCollection {
|
|
53
|
+
readonly sites: ReadonlySet<string>;
|
|
54
|
+
readonly callSignaturesBySite: ReadonlyMap<string, CallableFlowExpectedSignature>;
|
|
47
55
|
}
|
|
48
56
|
/** Position key shared with the existing free/reference skip-set contract. */
|
|
49
57
|
export declare function callableFlowSiteKey(filePath: string, range: {
|
|
@@ -55,6 +63,8 @@ export declare function callableFlowSiteKey(filePath: string, range: {
|
|
|
55
63
|
* Malformed/stale facts never suppress ordinary resolution.
|
|
56
64
|
*/
|
|
57
65
|
export declare function collectDeferredIndirectSites(parsedFiles: readonly ParsedFile[], scopes?: ScopeResolutionIndexes): ReadonlySet<string>;
|
|
66
|
+
/** One `referenceSites` walk for deferred keys and call-signature evidence. */
|
|
67
|
+
export declare function collectDeferredIndirectCollection(parsedFiles: readonly ParsedFile[], scopes?: ScopeResolutionIndexes): DeferredIndirectCollection;
|
|
58
68
|
export declare function emitCallableValueFlow(input: EmitCallableValueFlowInput): CallableValueFlowResult;
|
|
59
69
|
/**
|
|
60
70
|
* Pure — exported for `bench/callable-value-flow/measure.mjs`, which guards its
|
|
@@ -28,7 +28,12 @@ export function callableFlowSiteKey(filePath, range) {
|
|
|
28
28
|
* Malformed/stale facts never suppress ordinary resolution.
|
|
29
29
|
*/
|
|
30
30
|
export function collectDeferredIndirectSites(parsedFiles, scopes) {
|
|
31
|
+
return collectDeferredIndirectCollection(parsedFiles, scopes).sites;
|
|
32
|
+
}
|
|
33
|
+
/** One `referenceSites` walk for deferred keys and call-signature evidence. */
|
|
34
|
+
export function collectDeferredIndirectCollection(parsedFiles, scopes) {
|
|
31
35
|
const out = new Set();
|
|
36
|
+
const callSignaturesBySite = new Map();
|
|
32
37
|
const flowCells = new Set();
|
|
33
38
|
if (scopes !== undefined) {
|
|
34
39
|
for (const parsed of parsedFiles) {
|
|
@@ -41,9 +46,24 @@ export function collectDeferredIndirectSites(parsedFiles, scopes) {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
for (const parsed of parsedFiles) {
|
|
44
|
-
const canonical = new Set(
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
const canonical = new Set();
|
|
50
|
+
for (const site of parsed.referenceSites) {
|
|
51
|
+
if (site.kind !== 'call')
|
|
52
|
+
continue;
|
|
53
|
+
const key = callableFlowSiteKey(parsed.filePath, site.atRange);
|
|
54
|
+
canonical.add(key);
|
|
55
|
+
const signature = {
|
|
56
|
+
...(site.arity !== undefined ? { parameterCount: site.arity } : {}),
|
|
57
|
+
...(site.argumentTypes !== undefined ? { parameterTypes: site.argumentTypes } : {}),
|
|
58
|
+
...(site.argumentTypeClasses !== undefined
|
|
59
|
+
? { parameterTypeClasses: site.argumentTypeClasses }
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
const previous = callSignaturesBySite.get(key);
|
|
63
|
+
if (previous === undefined || signatureEvidence(signature) > signatureEvidence(previous)) {
|
|
64
|
+
callSignaturesBySite.set(key, signature);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
47
67
|
for (const site of parsed.callableFlowSites ?? []) {
|
|
48
68
|
if (site.kind !== 'invoke')
|
|
49
69
|
continue;
|
|
@@ -58,7 +78,7 @@ export function collectDeferredIndirectSites(parsedFiles, scopes) {
|
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
|
-
return out;
|
|
81
|
+
return { sites: out, callSignaturesBySite };
|
|
62
82
|
}
|
|
63
83
|
function flowCellOperand(site) {
|
|
64
84
|
switch (site.kind) {
|
|
@@ -80,7 +100,7 @@ function flowCellOperand(site) {
|
|
|
80
100
|
export function emitCallableValueFlow(input) {
|
|
81
101
|
const facts = [];
|
|
82
102
|
const invokes = [];
|
|
83
|
-
const canonicalInvokeKeys = collectDeferredIndirectSites(input.parsedFiles, input.scopes);
|
|
103
|
+
const canonicalInvokeKeys = input.deferredIndirectSites ?? collectDeferredIndirectSites(input.parsedFiles, input.scopes);
|
|
84
104
|
let unmatchedInvokes = 0;
|
|
85
105
|
for (const parsed of input.parsedFiles) {
|
|
86
106
|
for (const site of parsed.callableFlowSites ?? []) {
|
|
@@ -347,7 +367,7 @@ export function emitCallableValueFlow(input) {
|
|
|
347
367
|
addAddress(bindingKey(fact.filePath, fact.site.destination), bindingKey(fact.filePath, fact.site.source), `address:${fact.filePath}:${fact.site.source.name}`);
|
|
348
368
|
}
|
|
349
369
|
const formalsByGraphId = indexFormalsByGraphId(input.parsedFiles, input.scopes, input.nodeLookup, targetIndexes, aliasesByTargetId);
|
|
350
|
-
const callSignaturesBySite = indexCallSignatures(input.parsedFiles);
|
|
370
|
+
const callSignaturesBySite = input.callSignaturesBySite ?? indexCallSignatures(input.parsedFiles);
|
|
351
371
|
const dynamicCallees = new Map();
|
|
352
372
|
const dynamicOverflow = new Set();
|
|
353
373
|
const dynamicTargetHistory = new Map();
|
|
@@ -49,7 +49,7 @@ import { emitReturnShapeMemberAccesses } from '../passes/return-shape-members.js
|
|
|
49
49
|
import { emitImportedValueReferences } from '../passes/imported-value-refs.js';
|
|
50
50
|
import { createCalleeIdAccumulator, } from '../graph-bridge/callee-id-sink.js';
|
|
51
51
|
import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
|
|
52
|
-
import { callableFlowSiteKey,
|
|
52
|
+
import { callableFlowSiteKey, collectDeferredIndirectCollection, emitCallableValueFlow, } from '../passes/callable-value-flow.js';
|
|
53
53
|
import { findEnclosingClassDef, resolveInheritanceBaseInScope } from '../scope/walkers.js';
|
|
54
54
|
import { heritageTypeArgumentsKey, } from '../utils/generic-instantiation.js';
|
|
55
55
|
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
|
|
@@ -614,7 +614,8 @@ export function runScopeResolution(input, provider) {
|
|
|
614
614
|
// ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ────────────
|
|
615
615
|
input.onProgress?.('linking symbols', files.length, files.length);
|
|
616
616
|
const handledSites = new Set(preEmittedInheritanceSites);
|
|
617
|
-
const
|
|
617
|
+
const deferredIndirectCollection = collectDeferredIndirectCollection(emitParsedFiles, indexes);
|
|
618
|
+
const deferredIndirectSites = deferredIndirectCollection.sites;
|
|
618
619
|
const callableArgumentSites = new Set();
|
|
619
620
|
if (input.pdg !== true && deferredIndirectSites.size > 0) {
|
|
620
621
|
for (const parsed of emitParsedFiles) {
|
|
@@ -788,6 +789,8 @@ export function runScopeResolution(input, provider) {
|
|
|
788
789
|
collapseByCallerTarget: provider.collapseMemberCallsByCallerTarget === true,
|
|
789
790
|
isCallableValueTarget: provider.isCallableValueTarget,
|
|
790
791
|
hasFileLocalCallableLinkage: provider.hasFileLocalCallableLinkage,
|
|
792
|
+
deferredIndirectSites,
|
|
793
|
+
callSignaturesBySite: deferredIndirectCollection.callSignaturesBySite,
|
|
791
794
|
onWarn: (warning) => logger.warn(warning, 'callable-value-flow: candidate set exceeded the cap; no partial CALLS emitted'),
|
|
792
795
|
});
|
|
793
796
|
const importsEmitted = callableFlowOnly
|
|
@@ -152,14 +152,15 @@ export const isBinaryContent = (content) => {
|
|
|
152
152
|
}
|
|
153
153
|
return nonPrintable / end > 0.1;
|
|
154
154
|
};
|
|
155
|
+
const EMPTY_PREPARED = { content: '', lines: [''], isBinary: false };
|
|
155
156
|
/**
|
|
156
157
|
* LRU content cache — avoids re-reading the same source file for every
|
|
157
158
|
* symbol defined in it. Sized generously so most files stay cached during
|
|
158
|
-
* the single-pass node iteration.
|
|
159
|
+
* the single-pass node iteration. Insertion order on the Map is LRU
|
|
160
|
+
* (delete+set on touch).
|
|
159
161
|
*/
|
|
160
162
|
class FileContentCache {
|
|
161
163
|
cache = new Map();
|
|
162
|
-
accessOrder = [];
|
|
163
164
|
maxSize;
|
|
164
165
|
repoPath;
|
|
165
166
|
constructor(repoPath, maxSize = 3000) {
|
|
@@ -168,36 +169,36 @@ class FileContentCache {
|
|
|
168
169
|
}
|
|
169
170
|
async get(relativePath) {
|
|
170
171
|
if (!relativePath)
|
|
171
|
-
return
|
|
172
|
+
return EMPTY_PREPARED;
|
|
172
173
|
const cached = this.cache.get(relativePath);
|
|
173
174
|
if (cached !== undefined) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (idx !== -1) {
|
|
177
|
-
this.accessOrder.splice(idx, 1);
|
|
178
|
-
this.accessOrder.push(relativePath);
|
|
179
|
-
}
|
|
175
|
+
this.cache.delete(relativePath);
|
|
176
|
+
this.cache.set(relativePath, cached);
|
|
180
177
|
return cached;
|
|
181
178
|
}
|
|
182
179
|
try {
|
|
183
180
|
const fullPath = path.join(this.repoPath, relativePath);
|
|
184
181
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
const prepared = {
|
|
183
|
+
content,
|
|
184
|
+
lines: content.split('\n'),
|
|
185
|
+
isBinary: isBinaryContent(content),
|
|
186
|
+
};
|
|
187
|
+
this.set(relativePath, prepared);
|
|
188
|
+
return prepared;
|
|
187
189
|
}
|
|
188
190
|
catch {
|
|
189
|
-
this.set(relativePath,
|
|
190
|
-
return
|
|
191
|
+
this.set(relativePath, EMPTY_PREPARED);
|
|
192
|
+
return EMPTY_PREPARED;
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
set(key, value) {
|
|
194
|
-
if (this.cache.size >= this.maxSize) {
|
|
195
|
-
const oldest = this.
|
|
196
|
-
if (oldest)
|
|
196
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
197
|
+
const oldest = this.cache.keys().next().value;
|
|
198
|
+
if (oldest !== undefined)
|
|
197
199
|
this.cache.delete(oldest);
|
|
198
200
|
}
|
|
199
201
|
this.cache.set(key, value);
|
|
200
|
-
this.accessOrder.push(key);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
/**
|
|
@@ -247,12 +248,13 @@ const formatFtsDescription = (description) => isBinaryContent(description) ? ''
|
|
|
247
248
|
const EXACT_SYMBOL_CONTENT_LABELS = SYMBOL_NODE_LABELS;
|
|
248
249
|
const extractContent = async (node, contentCache) => {
|
|
249
250
|
const filePath = node.properties.filePath;
|
|
250
|
-
const
|
|
251
|
+
const prepared = await contentCache.get(filePath);
|
|
252
|
+
const content = prepared.content;
|
|
251
253
|
if (!content)
|
|
252
254
|
return '';
|
|
253
255
|
if (node.label === 'Folder')
|
|
254
256
|
return '';
|
|
255
|
-
if (
|
|
257
|
+
if (prepared.isBinary)
|
|
256
258
|
return '[Binary file - content not stored]';
|
|
257
259
|
// File content is stored in full — intentionally NOT length-capped here, so
|
|
258
260
|
// text past the old 10KB cutoff stays FTS-searchable (#2317). It is already
|
|
@@ -266,7 +268,7 @@ const extractContent = async (node, contentCache) => {
|
|
|
266
268
|
const endLine = node.properties.endLine;
|
|
267
269
|
if (startLine === undefined || endLine === undefined)
|
|
268
270
|
return '';
|
|
269
|
-
const lines =
|
|
271
|
+
const lines = prepared.lines;
|
|
270
272
|
const exactSymbolContent = EXACT_SYMBOL_CONTENT_LABELS.has(node.label);
|
|
271
273
|
const start = Math.max(0, exactSymbolContent ? startLine : startLine - 2);
|
|
272
274
|
const end = Math.min(lines.length - 1, exactSymbolContent ? endLine : endLine + 2);
|
package/package.json
CHANGED