@quolu/lattice 0.43.0 → 0.45.0
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/package.json +4 -2
- package/sensor/NOTICE +8 -19
- package/sensor/dist/bin/lattice-sensor.js +31 -0
- package/sensor/dist/db/index.d.ts +114 -0
- package/sensor/dist/db/index.d.ts.map +1 -1
- package/sensor/dist/db/index.js +235 -7
- package/sensor/dist/db/index.js.map +1 -1
- package/sensor/dist/db/migrations.js +1 -1
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +46 -8
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +99 -16
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/wal-valve.d.ts +15 -2
- package/sensor/dist/db/wal-valve.d.ts.map +1 -1
- package/sensor/dist/db/wal-valve.js +88 -6
- package/sensor/dist/db/wal-valve.js.map +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
- package/sensor/dist/extraction/extraction-version.js +5 -1
- package/sensor/dist/extraction/extraction-version.js.map +1 -1
- package/sensor/dist/extraction/function-ref.d.ts.map +1 -1
- package/sensor/dist/extraction/function-ref.js +6 -0
- package/sensor/dist/extraction/function-ref.js.map +1 -1
- package/sensor/dist/extraction/grammars.d.ts.map +1 -1
- package/sensor/dist/extraction/grammars.js +46 -0
- package/sensor/dist/extraction/grammars.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +11 -1
- package/sensor/dist/extraction/index.d.ts.map +1 -1
- package/sensor/dist/extraction/index.js +68 -16
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/decode.d.ts.map +1 -1
- package/sensor/dist/extraction/kernel/decode.js +9 -3
- package/sensor/dist/extraction/kernel/decode.js.map +1 -1
- package/sensor/dist/extraction/kernel/index.d.ts +2 -0
- package/sensor/dist/extraction/kernel/index.d.ts.map +1 -1
- package/sensor/dist/extraction/kernel/index.js +122 -8
- package/sensor/dist/extraction/kernel/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/layout.d.ts +10 -1
- package/sensor/dist/extraction/kernel/layout.d.ts.map +1 -1
- package/sensor/dist/extraction/kernel/layout.js +11 -2
- package/sensor/dist/extraction/kernel/layout.js.map +1 -1
- package/sensor/dist/extraction/kernel/loader.d.ts +42 -0
- package/sensor/dist/extraction/kernel/loader.d.ts.map +1 -1
- package/sensor/dist/extraction/kernel/loader.js.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.d.ts +13 -0
- package/sensor/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.js +809 -13
- package/sensor/dist/extraction/languages/c-cpp.js.map +1 -1
- package/sensor/dist/extraction/store-worker.js +29 -0
- package/sensor/dist/extraction/store-worker.js.map +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts +4 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +20 -4
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
- package/sensor/dist/index.d.ts +12 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +42 -7
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/mcp/daemon-registry.d.ts +5 -2
- package/sensor/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-registry.js +5 -2
- package/sensor/dist/mcp/daemon-registry.js.map +1 -1
- package/sensor/dist/mcp/index.d.ts +24 -0
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +51 -2
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.js +3 -1
- package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
- package/sensor/dist/mcp/server-instructions.js +1 -0
- package/sensor/dist/mcp/server-instructions.js.map +1 -1
- package/sensor/dist/mcp/tools.d.ts +48 -0
- package/sensor/dist/mcp/tools.d.ts.map +1 -1
- package/sensor/dist/mcp/tools.js +231 -4
- package/sensor/dist/mcp/tools.js.map +1 -1
- package/sensor/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/c-fnptr-synthesizer.js +561 -127
- package/sensor/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.js +80 -11
- package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +34 -2
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts +34 -0
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +393 -51
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/memory-budget.d.ts +48 -0
- package/sensor/dist/resolution/memory-budget.d.ts.map +1 -0
- package/sensor/dist/resolution/memory-budget.js +162 -0
- package/sensor/dist/resolution/memory-budget.js.map +1 -0
- package/sensor/dist/resolution/name-matcher.d.ts +4 -7
- package/sensor/dist/resolution/name-matcher.d.ts.map +1 -1
- package/sensor/dist/resolution/name-matcher.js +256 -91
- package/sensor/dist/resolution/name-matcher.js.map +1 -1
- package/sensor/dist/resolution/resolver-pool.d.ts +41 -1
- package/sensor/dist/resolution/resolver-pool.d.ts.map +1 -1
- package/sensor/dist/resolution/resolver-pool.js +103 -3
- package/sensor/dist/resolution/resolver-pool.js.map +1 -1
- package/sensor/dist/resolution/resolver-worker.js +27 -0
- package/sensor/dist/resolution/resolver-worker.js.map +1 -1
- package/sensor/dist/sync/watcher.d.ts +8 -1
- package/sensor/dist/sync/watcher.d.ts.map +1 -1
- package/sensor/dist/sync/watcher.js +44 -2
- package/sensor/dist/sync/watcher.js.map +1 -1
- package/sensor/dist/types.d.ts +7 -3
- package/sensor/dist/types.d.ts.map +1 -1
- package/src/cli-help.mjs +16 -0
- package/src/sensor-adapter.mjs +4 -0
- package/src/sensor-cli.mjs +37 -5
- package/src/sensor-diff.mjs +661 -0
|
@@ -18,9 +18,11 @@ exports.matchCppCallChain = matchCppCallChain;
|
|
|
18
18
|
exports.matchScopedCallChain = matchScopedCallChain;
|
|
19
19
|
exports.matchDottedCallChain = matchDottedCallChain;
|
|
20
20
|
exports.normalizeInferredTypeName = normalizeInferredTypeName;
|
|
21
|
+
exports.clearNameMatcherMemos = clearNameMatcherMemos;
|
|
21
22
|
exports.localReceiverTypePatterns = localReceiverTypePatterns;
|
|
22
23
|
exports.matchMethodCall = matchMethodCall;
|
|
23
24
|
exports.matchFuzzy = matchFuzzy;
|
|
25
|
+
exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
|
|
24
26
|
exports.matchReference = matchReference;
|
|
25
27
|
/**
|
|
26
28
|
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
|
|
@@ -229,6 +231,15 @@ function matchFunctionRef(ref, context) {
|
|
|
229
231
|
ref.language === 'arkts' ||
|
|
230
232
|
ref.language === 'cpp' || ref.language === 'python' ||
|
|
231
233
|
ref.language === 'php';
|
|
234
|
+
// Python additionally accepts CLASS targets for bare identifiers (#1478):
|
|
235
|
+
// class-as-value is a core Python idiom (`return SomeSerializer`,
|
|
236
|
+
// `Meta.model = Org`, registry dicts, `admin.site.register(Model, Admin)`)
|
|
237
|
+
// and, unlike TS, Python has no type-annotation recovery path. The
|
|
238
|
+
// false-positive mechanism behind the function-only rule was lowercase
|
|
239
|
+
// locals colliding with same-named METHODS (docopt.py) — a candidate must
|
|
240
|
+
// be an exact-name CLASS node here, and the extraction gate (same-file
|
|
241
|
+
// class ∪ imports) plus unique-or-drop still apply. Methods stay excluded.
|
|
242
|
+
const bareClassOk = ref.language === 'python';
|
|
232
243
|
// Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
|
|
233
244
|
// resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m`
|
|
234
245
|
// shape is an explicit member reference). Unique-or-drop like everything else.
|
|
@@ -257,7 +268,9 @@ function matchFunctionRef(ref, context) {
|
|
|
257
268
|
}
|
|
258
269
|
let candidates = context
|
|
259
270
|
.getNodesByName(ref.referenceName)
|
|
260
|
-
.filter((n) => (n.kind === 'function' ||
|
|
271
|
+
.filter((n) => (n.kind === 'function' ||
|
|
272
|
+
(!bareFnOnly && n.kind === 'method') ||
|
|
273
|
+
(bareClassOk && n.kind === 'class')) &&
|
|
261
274
|
sameLanguageFamily(n.language, ref.language) &&
|
|
262
275
|
n.id !== ref.fromNodeId // a function registering itself is not a dependency edge
|
|
263
276
|
);
|
|
@@ -546,11 +559,16 @@ depth = 0) {
|
|
|
546
559
|
// populated in the conformance pass. Still VALIDATED (the method must exist on
|
|
547
560
|
// a supertype), so a wrong inference produces no edge.
|
|
548
561
|
if (depth < 4 && context.getSupertypes) {
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
562
|
+
const viaSupers = nmTimedT('rmot-supers', ref, () => {
|
|
563
|
+
for (const supertype of context.getSupertypes(typeName, ref.language)) {
|
|
564
|
+
const via = resolveMethodOnType(supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1);
|
|
565
|
+
if (via)
|
|
566
|
+
return via;
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
});
|
|
570
|
+
if (viaSupers)
|
|
571
|
+
return viaSupers;
|
|
554
572
|
}
|
|
555
573
|
return null;
|
|
556
574
|
}
|
|
@@ -1032,7 +1050,50 @@ function normalizeInferredTypeName(raw) {
|
|
|
1032
1050
|
* PascalCase is required in the capture where the language convention allows,
|
|
1033
1051
|
* as a cheap false-positive guard on top of resolveMethodOnType's validation.
|
|
1034
1052
|
*/
|
|
1053
|
+
/**
|
|
1054
|
+
* Compiled-pattern memo for the receiver-type pattern builders below. They
|
|
1055
|
+
* run for EVERY `receiver.method()` ref the matcher attempts, compiling 2–4
|
|
1056
|
+
* fresh RegExp objects per call — and receivers repeat massively (`self`
|
|
1057
|
+
* alone accounts for tens of thousands of refs on a Lua repo, measured 41µs
|
|
1058
|
+
* per methodCall miss on kong with compilation a large slice). The patterns
|
|
1059
|
+
* are a pure function of (language, receiver) and non-global (`.match()`
|
|
1060
|
+
* never touches lastIndex), so shared instances are behavior-identical.
|
|
1061
|
+
* FIFO-capped with no per-get mutation (the §7a.6 LRU-churn lesson): a hit
|
|
1062
|
+
* costs one Map lookup, overflow evicts oldest, and an evicted entry simply
|
|
1063
|
+
* recompiles exactly as every call did before this memo.
|
|
1064
|
+
*/
|
|
1065
|
+
const PATTERN_MEMO = new Map();
|
|
1066
|
+
const PATTERN_MEMO_CAP = 8192;
|
|
1067
|
+
const INFER_SCAN_STATES = new WeakMap();
|
|
1068
|
+
function getInferScanStates(context) {
|
|
1069
|
+
let m = INFER_SCAN_STATES.get(context);
|
|
1070
|
+
if (!m) {
|
|
1071
|
+
m = new Map();
|
|
1072
|
+
INFER_SCAN_STATES.set(context, m);
|
|
1073
|
+
}
|
|
1074
|
+
return m;
|
|
1075
|
+
}
|
|
1076
|
+
/** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
|
|
1077
|
+
function clearNameMatcherMemos(context) {
|
|
1078
|
+
INFER_SCAN_STATES.delete(context);
|
|
1079
|
+
}
|
|
1080
|
+
function memoPatterns(key, build) {
|
|
1081
|
+
const hit = PATTERN_MEMO.get(key);
|
|
1082
|
+
if (hit)
|
|
1083
|
+
return hit;
|
|
1084
|
+
const patterns = build();
|
|
1085
|
+
if (PATTERN_MEMO.size >= PATTERN_MEMO_CAP) {
|
|
1086
|
+
const oldest = PATTERN_MEMO.keys().next().value;
|
|
1087
|
+
if (oldest !== undefined)
|
|
1088
|
+
PATTERN_MEMO.delete(oldest);
|
|
1089
|
+
}
|
|
1090
|
+
PATTERN_MEMO.set(key, patterns);
|
|
1091
|
+
return patterns;
|
|
1092
|
+
}
|
|
1035
1093
|
function localReceiverTypePatterns(language, r) {
|
|
1094
|
+
return memoPatterns(`${language}|${r}`, () => buildLocalReceiverTypePatterns(language, r));
|
|
1095
|
+
}
|
|
1096
|
+
function buildLocalReceiverTypePatterns(language, r) {
|
|
1036
1097
|
switch (language) {
|
|
1037
1098
|
case 'typescript':
|
|
1038
1099
|
case 'javascript':
|
|
@@ -1275,6 +1336,53 @@ function inferLocalReceiverType(receiverName, ref, context) {
|
|
|
1275
1336
|
}
|
|
1276
1337
|
return null;
|
|
1277
1338
|
};
|
|
1339
|
+
// Incremental-scan memo (INFER_SCAN_STATES): this scan runs for EVERY
|
|
1340
|
+
// `receiver.method()` ref and was measured at 61µs/ref on kong (2.4s of
|
|
1341
|
+
// worker time, 99% misses — `self:` calls hunting a declaration Lua never
|
|
1342
|
+
// writes). Refs for the same (file, scope, receiver) arrive in ~ascending
|
|
1343
|
+
// line order, and the scan is a pure function of the file's immutable
|
|
1344
|
+
// lines, so each line pays its regex matches ONCE per key instead of once
|
|
1345
|
+
// per ref: query(c) = highest matching line in [startIdx..c]; a monotonic
|
|
1346
|
+
// call extends the stored watermark by scanning only (hi..c] (the region
|
|
1347
|
+
// at-or-below the previous answer is already proven empty above it); a
|
|
1348
|
+
// non-monotonic call (rare — refs are rowid-ordered) falls back to the
|
|
1349
|
+
// plain bounded scan and leaves the state alone. componentScoped is keyed
|
|
1350
|
+
// out — its position-independent whole-file sweep below has different
|
|
1351
|
+
// semantics.
|
|
1352
|
+
if (!componentScoped) {
|
|
1353
|
+
const states = getInferScanStates(context);
|
|
1354
|
+
const key = `${ref.filePath}|${startIdx}|${ref.language}|${scanReceiver}`;
|
|
1355
|
+
const state = states.get(key);
|
|
1356
|
+
if (!state) {
|
|
1357
|
+
for (let i = callIdx; i >= startIdx; i--) {
|
|
1358
|
+
const type = matchLine(i);
|
|
1359
|
+
if (type) {
|
|
1360
|
+
states.set(key, { hi: callIdx, ansIdx: i, ansType: type });
|
|
1361
|
+
return type;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
states.set(key, { hi: callIdx, ansIdx: -1, ansType: null });
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
if (callIdx >= state.hi) {
|
|
1368
|
+
for (let i = callIdx; i > state.hi; i--) {
|
|
1369
|
+
const type = matchLine(i);
|
|
1370
|
+
if (type) {
|
|
1371
|
+
state.ansIdx = i;
|
|
1372
|
+
state.ansType = type;
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
state.hi = callIdx;
|
|
1377
|
+
return state.ansIdx >= startIdx ? state.ansType : null;
|
|
1378
|
+
}
|
|
1379
|
+
for (let i = callIdx; i >= startIdx; i--) {
|
|
1380
|
+
const type = matchLine(i);
|
|
1381
|
+
if (type)
|
|
1382
|
+
return type;
|
|
1383
|
+
}
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1278
1386
|
// Nearest declaration wins: scan backward from the call to the scope start.
|
|
1279
1387
|
for (let i = callIdx; i >= startIdx; i--) {
|
|
1280
1388
|
const type = matchLine(i);
|
|
@@ -1317,6 +1425,9 @@ function inferLocalReceiverType(receiverName, ref, context) {
|
|
|
1317
1425
|
* shape is handled by inferPhpAssignedPropertyType instead.
|
|
1318
1426
|
*/
|
|
1319
1427
|
function phpPropertyTypePatterns(r) {
|
|
1428
|
+
return memoPatterns(`php-prop|${r}`, () => buildPhpPropertyTypePatterns(r));
|
|
1429
|
+
}
|
|
1430
|
+
function buildPhpPropertyTypePatterns(r) {
|
|
1320
1431
|
return [
|
|
1321
1432
|
new RegExp(`\\b(?:(?:private|protected|public|readonly|static|final)(?:\\(set\\))?\\s+)+\\??([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // private readonly ?Foo $prop (typed property / promoted param)
|
|
1322
1433
|
new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
|
|
@@ -1450,9 +1561,9 @@ function matchMethodCall(ref, context) {
|
|
|
1450
1561
|
// shared source-based inferrer. resolveMethodOnType validates the method
|
|
1451
1562
|
// exists on the inferred type, so a mis-inference produces no edge.
|
|
1452
1563
|
if (inferableReceiver) {
|
|
1453
|
-
const inferredType = ref.language === 'cpp'
|
|
1564
|
+
const inferredType = nmTimedT('mc-infer', ref, () => ref.language === 'cpp'
|
|
1454
1565
|
? inferCppReceiverType(objectOrClass, ref, context)
|
|
1455
|
-
: inferLocalReceiverType(objectOrClass, ref, context);
|
|
1566
|
+
: inferLocalReceiverType(objectOrClass, ref, context));
|
|
1456
1567
|
if (inferredType) {
|
|
1457
1568
|
// Java/Kotlin: when two classes share the simple name, the file's import
|
|
1458
1569
|
// pins WHICH one (#314). Other languages disambiguate by call-site file.
|
|
@@ -1461,7 +1572,7 @@ function matchMethodCall(ref, context) {
|
|
|
1461
1572
|
.getImportMappings(ref.filePath, ref.language)
|
|
1462
1573
|
.find((i) => i.localName === inferredType)?.source
|
|
1463
1574
|
: undefined;
|
|
1464
|
-
const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
|
|
1575
|
+
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
|
|
1465
1576
|
if (typedMatch) {
|
|
1466
1577
|
return typedMatch;
|
|
1467
1578
|
}
|
|
@@ -1494,7 +1605,7 @@ function matchMethodCall(ref, context) {
|
|
|
1494
1605
|
// imported FQN so resolveMethodOnType can disambiguate (#314).
|
|
1495
1606
|
const imports = context.getImportMappings(ref.filePath, ref.language);
|
|
1496
1607
|
const importedFqn = imports.find((i) => i.localName === inferredType)?.source;
|
|
1497
|
-
const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
|
|
1608
|
+
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
|
|
1498
1609
|
if (typedMatch) {
|
|
1499
1610
|
return typedMatch;
|
|
1500
1611
|
}
|
|
@@ -1505,32 +1616,9 @@ function matchMethodCall(ref, context) {
|
|
|
1505
1616
|
// with a `Logger` in both `a/` and `b/`), try the class in the call site's
|
|
1506
1617
|
// own file first — otherwise the first-indexed class wins and a call in `b/`
|
|
1507
1618
|
// resolves to `a/`'s method (#1079).
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
// Skip cross-language class matches
|
|
1512
|
-
if (classNode.language !== ref.language)
|
|
1513
|
-
continue;
|
|
1514
|
-
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
1515
|
-
const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
|
|
1516
|
-
n.name === methodName &&
|
|
1517
|
-
n.qualifiedName.includes(classNode.name));
|
|
1518
|
-
if (methodNode) {
|
|
1519
|
-
return {
|
|
1520
|
-
original: ref,
|
|
1521
|
-
targetNodeId: methodNode.id,
|
|
1522
|
-
confidence: 0.85,
|
|
1523
|
-
resolvedBy: 'qualified-name',
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
|
1529
|
-
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
|
1530
|
-
const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
|
|
1531
|
-
if (capitalizedReceiver !== objectOrClass) {
|
|
1532
|
-
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
|
|
1533
|
-
for (const classNode of fuzzyClassCandidates) {
|
|
1619
|
+
const strat1 = nmTimedT('mc-class', ref, () => {
|
|
1620
|
+
const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
|
|
1621
|
+
for (const classNode of classCandidates) {
|
|
1534
1622
|
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
|
1535
1623
|
// Skip cross-language class matches
|
|
1536
1624
|
if (classNode.language !== ref.language)
|
|
@@ -1543,68 +1631,106 @@ function matchMethodCall(ref, context) {
|
|
|
1543
1631
|
return {
|
|
1544
1632
|
original: ref,
|
|
1545
1633
|
targetNodeId: methodNode.id,
|
|
1546
|
-
confidence: 0.
|
|
1547
|
-
resolvedBy: '
|
|
1634
|
+
confidence: 0.85,
|
|
1635
|
+
resolvedBy: 'qualified-name',
|
|
1548
1636
|
};
|
|
1549
1637
|
}
|
|
1550
1638
|
}
|
|
1551
1639
|
}
|
|
1640
|
+
return null;
|
|
1641
|
+
});
|
|
1642
|
+
if (strat1)
|
|
1643
|
+
return strat1;
|
|
1644
|
+
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
|
1645
|
+
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
|
1646
|
+
const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
|
|
1647
|
+
if (capitalizedReceiver !== objectOrClass) {
|
|
1648
|
+
const strat2 = nmTimedT('mc-capital', ref, () => {
|
|
1649
|
+
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
|
|
1650
|
+
for (const classNode of fuzzyClassCandidates) {
|
|
1651
|
+
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
|
1652
|
+
// Skip cross-language class matches
|
|
1653
|
+
if (classNode.language !== ref.language)
|
|
1654
|
+
continue;
|
|
1655
|
+
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
1656
|
+
const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
|
|
1657
|
+
n.name === methodName &&
|
|
1658
|
+
n.qualifiedName.includes(classNode.name));
|
|
1659
|
+
if (methodNode) {
|
|
1660
|
+
return {
|
|
1661
|
+
original: ref,
|
|
1662
|
+
targetNodeId: methodNode.id,
|
|
1663
|
+
confidence: 0.8,
|
|
1664
|
+
resolvedBy: 'instance-method',
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return null;
|
|
1670
|
+
});
|
|
1671
|
+
if (strat2)
|
|
1672
|
+
return strat2;
|
|
1552
1673
|
}
|
|
1553
1674
|
// Strategy 3: Find methods by name across the codebase, match by receiver
|
|
1554
1675
|
// name similarity with the containing class. Handles abbreviated variable
|
|
1555
1676
|
// names like permissionEngine → PermissionRuleEngine.
|
|
1556
1677
|
if (methodName) {
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
|
|
1568
|
-
// Filter to same-language candidates first
|
|
1569
|
-
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
|
1570
|
-
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
|
1571
|
-
// If only one same-language method with this name exists, use it
|
|
1572
|
-
if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
|
|
1573
|
-
return {
|
|
1574
|
-
original: ref,
|
|
1575
|
-
targetNodeId: targetMethods[0].id,
|
|
1576
|
-
confidence: 0.7,
|
|
1577
|
-
resolvedBy: 'instance-method',
|
|
1578
|
-
};
|
|
1579
|
-
}
|
|
1580
|
-
// Multiple methods: score by receiver name word overlap with class name
|
|
1581
|
-
if (targetMethods.length > 1) {
|
|
1582
|
-
const receiverWords = splitCamelCase(objectOrClass);
|
|
1583
|
-
let bestMatch;
|
|
1584
|
-
let bestScore = 0;
|
|
1585
|
-
// Same-file candidates first, so a score tie (`score > bestScore` keeps
|
|
1586
|
-
// the first seen) resolves to the call site's own file rather than the
|
|
1587
|
-
// first-indexed duplicate (#1079).
|
|
1588
|
-
for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
|
|
1589
|
-
const classWords = splitCamelCase(method.qualifiedName);
|
|
1590
|
-
let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
|
|
1591
|
-
// Bonus for same language
|
|
1592
|
-
if (method.language === ref.language)
|
|
1593
|
-
score += 1;
|
|
1594
|
-
if (score > bestScore) {
|
|
1595
|
-
bestScore = score;
|
|
1596
|
-
bestMatch = method;
|
|
1597
|
-
}
|
|
1678
|
+
const strat3 = nmTimedT('mc-byname', ref, () => {
|
|
1679
|
+
const methodCandidates = context.getNodesByName(methodName);
|
|
1680
|
+
// Ubiquitous-method ceiling (#999): a method name re-declared across a
|
|
1681
|
+
// vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
|
|
1682
|
+
// K candidates that receiver-word overlap can't reliably disambiguate —
|
|
1683
|
+
// and filtering + scoring all K per call is the O(K²) cost that wedged
|
|
1684
|
+
// "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
|
|
1685
|
+
// (class-name match) already had their precise shot above.
|
|
1686
|
+
if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
|
|
1687
|
+
return null;
|
|
1598
1688
|
}
|
|
1599
|
-
|
|
1689
|
+
const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
|
|
1690
|
+
// Filter to same-language candidates first
|
|
1691
|
+
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
|
1692
|
+
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
|
1693
|
+
// If only one same-language method with this name exists, use it
|
|
1694
|
+
if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
|
|
1600
1695
|
return {
|
|
1601
1696
|
original: ref,
|
|
1602
|
-
targetNodeId:
|
|
1603
|
-
confidence: 0.
|
|
1697
|
+
targetNodeId: targetMethods[0].id,
|
|
1698
|
+
confidence: 0.7,
|
|
1604
1699
|
resolvedBy: 'instance-method',
|
|
1605
1700
|
};
|
|
1606
1701
|
}
|
|
1607
|
-
|
|
1702
|
+
// Multiple methods: score by receiver name word overlap with class name
|
|
1703
|
+
if (targetMethods.length > 1) {
|
|
1704
|
+
const receiverWords = splitCamelCase(objectOrClass);
|
|
1705
|
+
let bestMatch;
|
|
1706
|
+
let bestScore = 0;
|
|
1707
|
+
// Same-file candidates first, so a score tie (`score > bestScore` keeps
|
|
1708
|
+
// the first seen) resolves to the call site's own file rather than the
|
|
1709
|
+
// first-indexed duplicate (#1079).
|
|
1710
|
+
for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
|
|
1711
|
+
const classWords = splitCamelCase(method.qualifiedName);
|
|
1712
|
+
let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
|
|
1713
|
+
// Bonus for same language
|
|
1714
|
+
if (method.language === ref.language)
|
|
1715
|
+
score += 1;
|
|
1716
|
+
if (score > bestScore) {
|
|
1717
|
+
bestScore = score;
|
|
1718
|
+
bestMatch = method;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (bestMatch && bestScore >= 2) {
|
|
1722
|
+
return {
|
|
1723
|
+
original: ref,
|
|
1724
|
+
targetNodeId: bestMatch.id,
|
|
1725
|
+
confidence: 0.65,
|
|
1726
|
+
resolvedBy: 'instance-method',
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
});
|
|
1732
|
+
if (strat3)
|
|
1733
|
+
return strat3;
|
|
1608
1734
|
}
|
|
1609
1735
|
return null;
|
|
1610
1736
|
}
|
|
@@ -1846,6 +1972,45 @@ function matchFuzzy(ref, context) {
|
|
|
1846
1972
|
*/
|
|
1847
1973
|
/** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
|
|
1848
1974
|
const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
|
|
1975
|
+
/**
|
|
1976
|
+
* LATTICE_SENSOR_RESOLVE_PROFILE=2 sub-stage attribution for matchReference's
|
|
1977
|
+
* strategy pipeline (`nm:<stage>|<refKind>|hit/miss`). Module-global because
|
|
1978
|
+
* the matcher is a free function; each thread (main + every pool worker) has
|
|
1979
|
+
* its own module instance, and dumpNameMatcherProfile is invoked from
|
|
1980
|
+
* ReferenceResolver.dumpResolveProfile so worker tables surface too.
|
|
1981
|
+
*/
|
|
1982
|
+
const NM_PROFILE = process.env.LATTICE_SENSOR_RESOLVE_PROFILE === '2' ? new Map() : null;
|
|
1983
|
+
function nmTimedT(stage, ref, fn) {
|
|
1984
|
+
if (!NM_PROFILE)
|
|
1985
|
+
return fn();
|
|
1986
|
+
const t0 = process.hrtime.bigint();
|
|
1987
|
+
const r = fn();
|
|
1988
|
+
const dt = process.hrtime.bigint() - t0;
|
|
1989
|
+
const key = `nm:${stage}|${ref.referenceKind}|${r ? 'hit' : 'miss'}`;
|
|
1990
|
+
const slot = NM_PROFILE.get(key);
|
|
1991
|
+
if (slot) {
|
|
1992
|
+
slot.n++;
|
|
1993
|
+
slot.ns += dt;
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
NM_PROFILE.set(key, { n: 1, ns: dt });
|
|
1997
|
+
}
|
|
1998
|
+
return r;
|
|
1999
|
+
}
|
|
2000
|
+
function nmTimed(stage, ref, fn) {
|
|
2001
|
+
return nmTimedT(stage, ref, fn);
|
|
2002
|
+
}
|
|
2003
|
+
/** Dump this thread's matchReference sub-stage table to stderr (no-op unless =2). */
|
|
2004
|
+
function dumpNameMatcherProfile(label) {
|
|
2005
|
+
if (!NM_PROFILE || NM_PROFILE.size === 0)
|
|
2006
|
+
return;
|
|
2007
|
+
const rows = [...NM_PROFILE.entries()]
|
|
2008
|
+
.map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1000000n) }))
|
|
2009
|
+
.sort((a, b) => b.ms - a.ms);
|
|
2010
|
+
for (const r of rows) {
|
|
2011
|
+
console.error(`[resolve-profile] ${label} ${r.k}: n=${r.n} total=${(r.ms / 1000).toFixed(1)}s avg=${((r.ms * 1000) / Math.max(1, r.n)).toFixed(0)}µs`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1849
2014
|
function matchReference(ref, context) {
|
|
1850
2015
|
// Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
|
|
1851
2016
|
// never the fuzzy/qualified fallthrough below (a wrong callback edge is
|
|
@@ -1907,18 +2072,18 @@ function matchReference(ref, context) {
|
|
|
1907
2072
|
// Try strategies in order of confidence
|
|
1908
2073
|
let result;
|
|
1909
2074
|
// 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
|
|
1910
|
-
result = matchByFilePath(ref, context);
|
|
2075
|
+
result = nmTimed('filePath', ref, () => matchByFilePath(ref, context));
|
|
1911
2076
|
if (result)
|
|
1912
2077
|
return result;
|
|
1913
2078
|
// 1. Qualified name match (highest confidence)
|
|
1914
|
-
result = matchByQualifiedName(ref, context);
|
|
2079
|
+
result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context));
|
|
1915
2080
|
if (result)
|
|
1916
2081
|
return result;
|
|
1917
2082
|
// 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
|
|
1918
2083
|
// encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
|
|
1919
2084
|
// receiver's type from what the inner call returns, then the method on it.
|
|
1920
2085
|
if (ref.language === 'cpp' || ref.language === 'c') {
|
|
1921
|
-
result = matchCppCallChain(ref, context);
|
|
2086
|
+
result = nmTimed('cppChain', ref, () => matchCppCallChain(ref, context));
|
|
1922
2087
|
if (result)
|
|
1923
2088
|
return result;
|
|
1924
2089
|
}
|
|
@@ -1927,7 +2092,7 @@ function matchReference(ref, context) {
|
|
|
1927
2092
|
// type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
|
|
1928
2093
|
// concrete return type.
|
|
1929
2094
|
if (ref.language === 'php' || ref.language === 'rust') {
|
|
1930
|
-
result = matchScopedCallChain(ref, context);
|
|
2095
|
+
result = nmTimed('scopedChain', ref, () => matchScopedCallChain(ref, context));
|
|
1931
2096
|
if (result)
|
|
1932
2097
|
return result;
|
|
1933
2098
|
}
|
|
@@ -1947,20 +2112,20 @@ function matchReference(ref, context) {
|
|
|
1947
2112
|
ref.language === 'dart' ||
|
|
1948
2113
|
ref.language === 'objc' ||
|
|
1949
2114
|
ref.language === 'pascal') {
|
|
1950
|
-
result = matchDottedCallChain(ref, context);
|
|
2115
|
+
result = nmTimed('dottedChain', ref, () => matchDottedCallChain(ref, context));
|
|
1951
2116
|
if (result)
|
|
1952
2117
|
return result;
|
|
1953
2118
|
}
|
|
1954
2119
|
// 2. Method call pattern
|
|
1955
|
-
result = matchMethodCall(ref, context);
|
|
2120
|
+
result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
|
|
1956
2121
|
if (result)
|
|
1957
2122
|
return result;
|
|
1958
2123
|
// 3. Exact name match
|
|
1959
|
-
result = matchByExactName(ref, context);
|
|
2124
|
+
result = nmTimed('exactName', ref, () => matchByExactName(ref, context));
|
|
1960
2125
|
if (result)
|
|
1961
2126
|
return result;
|
|
1962
2127
|
// 4. Fuzzy match (lowest confidence)
|
|
1963
|
-
result = matchFuzzy(ref, context);
|
|
2128
|
+
result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
|
|
1964
2129
|
if (result)
|
|
1965
2130
|
return result;
|
|
1966
2131
|
return null;
|