homegraph 1.4.1 → 1.5.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/README.md +48 -11
- package/dist/bin/homegraph.js +295 -72
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +32 -11
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +143 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +293 -16
- package/dist/db/index.js.map +1 -1
- package/dist/db/queries.d.ts +43 -4
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +138 -14
- package/dist/db/queries.js.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +24 -17
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +292 -36
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/db/wal-valve.d.ts +15 -2
- package/dist/db/wal-valve.d.ts.map +1 -1
- package/dist/db/wal-valve.js +88 -6
- package/dist/db/wal-valve.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.js +3 -2
- package/dist/extraction/arkts-batch-worker.js.map +1 -1
- package/dist/extraction/default-ignore.d.ts +39 -0
- package/dist/extraction/default-ignore.d.ts.map +1 -0
- package/dist/extraction/default-ignore.js +221 -0
- package/dist/extraction/default-ignore.js.map +1 -0
- package/dist/extraction/index.d.ts +9 -10
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +227 -323
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +90 -6
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1579 -128
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/store-worker.d.ts +20 -0
- package/dist/extraction/store-worker.d.ts.map +1 -0
- package/dist/extraction/store-worker.js +102 -0
- package/dist/extraction/store-worker.js.map +1 -0
- package/dist/extraction/store-writer.d.ts +48 -0
- package/dist/extraction/store-writer.d.ts.map +1 -0
- package/dist/extraction/store-writer.js +167 -0
- package/dist/extraction/store-writer.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +47 -0
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +55 -7
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -97
- package/dist/index.js.map +1 -1
- package/dist/mcp/query-worker.d.ts +2 -2
- package/dist/mcp/query-worker.js +2 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +42 -26
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +52 -16
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +234 -180
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +10 -2
- package/dist/resolution/cooperative-yield.d.ts.map +1 -1
- package/dist/resolution/cooperative-yield.js +6 -4
- package/dist/resolution/cooperative-yield.js.map +1 -1
- package/dist/resolution/frameworks/go.d.ts.map +1 -1
- package/dist/resolution/frameworks/go.js +29 -6
- package/dist/resolution/frameworks/go.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +2 -3
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +166 -7
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +87 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +739 -131
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/memory-budget.d.ts +48 -0
- package/dist/resolution/memory-budget.d.ts.map +1 -0
- package/dist/resolution/memory-budget.js +162 -0
- package/dist/resolution/memory-budget.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +12 -1
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +439 -97
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/resolver-pool.d.ts +106 -0
- package/dist/resolution/resolver-pool.d.ts.map +1 -0
- package/dist/resolution/resolver-pool.js +344 -0
- package/dist/resolution/resolver-pool.js.map +1 -0
- package/dist/resolution/resolver-worker.d.ts +17 -0
- package/dist/resolution/resolver-worker.d.ts.map +1 -0
- package/dist/resolution/resolver-worker.js +141 -0
- package/dist/resolution/resolver-worker.js.map +1 -0
- package/dist/resolution/types.d.ts +3 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/spec/build/diff-parser.d.ts +55 -0
- package/dist/spec/build/diff-parser.d.ts.map +1 -0
- package/dist/spec/build/diff-parser.js +205 -0
- package/dist/spec/build/diff-parser.js.map +1 -0
- package/dist/spec/build/git-scanner.d.ts +93 -0
- package/dist/spec/build/git-scanner.d.ts.map +1 -0
- package/dist/spec/build/git-scanner.js +254 -0
- package/dist/spec/build/git-scanner.js.map +1 -0
- package/dist/spec/build/pipeline.d.ts +54 -0
- package/dist/spec/build/pipeline.d.ts.map +1 -0
- package/dist/spec/build/pipeline.js +170 -0
- package/dist/spec/build/pipeline.js.map +1 -0
- package/dist/spec/build/scan.d.ts +48 -0
- package/dist/spec/build/scan.d.ts.map +1 -0
- package/dist/spec/build/scan.js +89 -0
- package/dist/spec/build/scan.js.map +1 -0
- package/dist/spec/build/scope-resolver.d.ts +50 -0
- package/dist/spec/build/scope-resolver.d.ts.map +1 -0
- package/dist/spec/build/scope-resolver.js +105 -0
- package/dist/spec/build/scope-resolver.js.map +1 -0
- package/dist/spec/build/spec-extractor.d.ts +74 -0
- package/dist/spec/build/spec-extractor.d.ts.map +1 -0
- package/dist/spec/build/spec-extractor.js +344 -0
- package/dist/spec/build/spec-extractor.js.map +1 -0
- package/dist/spec/config.d.ts +35 -1
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +30 -5
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +19 -50
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/index.d.ts +5 -3
- package/dist/spec/db/index.d.ts.map +1 -1
- package/dist/spec/db/index.js +16 -1
- package/dist/spec/db/index.js.map +1 -1
- package/dist/spec/db/persist.d.ts +45 -0
- package/dist/spec/db/persist.d.ts.map +1 -0
- package/dist/spec/db/persist.js +66 -0
- package/dist/spec/db/persist.js.map +1 -0
- package/dist/spec/db/relations.d.ts +80 -1
- package/dist/spec/db/relations.d.ts.map +1 -1
- package/dist/spec/db/relations.js +175 -4
- package/dist/spec/db/relations.js.map +1 -1
- package/dist/spec/db/schema.d.ts +4 -0
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +4 -0
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +5 -1
- package/dist/spec/db/spec-node.d.ts +5 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -1
- package/dist/spec/db/spec-node.js +11 -5
- package/dist/spec/db/spec-node.js.map +1 -1
- package/dist/spec/db/sql-utils.d.ts +11 -0
- package/dist/spec/db/sql-utils.d.ts.map +1 -0
- package/dist/spec/db/sql-utils.js +19 -0
- package/dist/spec/db/sql-utils.js.map +1 -0
- package/dist/spec/evolve/cluster-context.d.ts +30 -0
- package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
- package/dist/spec/evolve/cluster-context.js +78 -0
- package/dist/spec/evolve/cluster-context.js.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
- package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.js +93 -0
- package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +23 -7
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
- package/dist/spec/evolve/impact-locator.js +58 -14
- package/dist/spec/evolve/impact-locator.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +62 -25
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +436 -457
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +45 -45
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/git/commits.d.ts +85 -0
- package/dist/spec/git/commits.d.ts.map +1 -0
- package/dist/spec/git/commits.js +218 -0
- package/dist/spec/git/commits.js.map +1 -0
- package/dist/spec/git/exec.d.ts +13 -0
- package/dist/spec/git/exec.d.ts.map +1 -0
- package/dist/spec/git/exec.js +19 -0
- package/dist/spec/git/exec.js.map +1 -0
- package/dist/spec/git/index.d.ts +9 -0
- package/dist/spec/git/index.d.ts.map +1 -0
- package/dist/spec/git/index.js +22 -0
- package/dist/spec/git/index.js.map +1 -0
- package/dist/spec/git-utils.d.ts +8 -0
- package/dist/spec/git-utils.d.ts.map +1 -0
- package/dist/spec/git-utils.js +14 -0
- package/dist/spec/git-utils.js.map +1 -0
- package/dist/spec/graph/index.d.ts +8 -0
- package/dist/spec/graph/index.d.ts.map +1 -0
- package/dist/spec/graph/index.js +17 -0
- package/dist/spec/graph/index.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +6 -35
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +71 -248
- package/dist/spec/graph/queries.js.map +1 -1
- package/dist/spec/llm/agent-client.d.ts +59 -0
- package/dist/spec/llm/agent-client.d.ts.map +1 -0
- package/dist/spec/llm/agent-client.js +213 -0
- package/dist/spec/llm/agent-client.js.map +1 -0
- package/dist/spec/llm/agents/claude-code.d.ts +32 -0
- package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/claude-code.js +125 -0
- package/dist/spec/llm/agents/claude-code.js.map +1 -0
- package/dist/spec/llm/agents/codex.d.ts +32 -0
- package/dist/spec/llm/agents/codex.d.ts.map +1 -0
- package/dist/spec/llm/agents/codex.js +145 -0
- package/dist/spec/llm/agents/codex.js.map +1 -0
- package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
- package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
- package/dist/spec/llm/agents/detect-utils.js +91 -0
- package/dist/spec/llm/agents/detect-utils.js.map +1 -0
- package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
- package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/deveco-code.js +165 -0
- package/dist/spec/llm/agents/deveco-code.js.map +1 -0
- package/dist/spec/llm/agents/index.d.ts +31 -0
- package/dist/spec/llm/agents/index.d.ts.map +1 -0
- package/dist/spec/llm/agents/index.js +65 -0
- package/dist/spec/llm/agents/index.js.map +1 -0
- package/dist/spec/llm/agents/types.d.ts +76 -0
- package/dist/spec/llm/agents/types.d.ts.map +1 -0
- package/dist/spec/llm/agents/types.js +15 -0
- package/dist/spec/llm/agents/types.js.map +1 -0
- package/dist/spec/llm/client.d.ts +31 -11
- package/dist/spec/llm/client.d.ts.map +1 -1
- package/dist/spec/llm/client.js +143 -81
- package/dist/spec/llm/client.js.map +1 -1
- package/dist/spec/llm/factory.d.ts +41 -0
- package/dist/spec/llm/factory.d.ts.map +1 -0
- package/dist/spec/llm/factory.js +80 -0
- package/dist/spec/llm/factory.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +24 -8
- package/dist/spec/llm/prompts.d.ts.map +1 -1
- package/dist/spec/llm/prompts.js +100 -50
- package/dist/spec/llm/prompts.js.map +1 -1
- package/dist/spec/llm/retry.d.ts +53 -0
- package/dist/spec/llm/retry.d.ts.map +1 -0
- package/dist/spec/llm/retry.js +153 -0
- package/dist/spec/llm/retry.js.map +1 -0
- package/dist/spec/mine/clusterer.d.ts +63 -0
- package/dist/spec/mine/clusterer.d.ts.map +1 -0
- package/dist/spec/mine/clusterer.js +904 -0
- package/dist/spec/mine/clusterer.js.map +1 -0
- package/dist/spec/mine/clustering/features.d.ts +38 -0
- package/dist/spec/mine/clustering/features.d.ts.map +1 -0
- package/dist/spec/mine/clustering/features.js +108 -0
- package/dist/spec/mine/clustering/features.js.map +1 -0
- package/dist/spec/mine/clustering/index.d.ts +62 -0
- package/dist/spec/mine/clustering/index.d.ts.map +1 -0
- package/dist/spec/mine/clustering/index.js +259 -0
- package/dist/spec/mine/clustering/index.js.map +1 -0
- package/dist/spec/mine/clustering/leiden.d.ts +61 -0
- package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
- package/dist/spec/mine/clustering/leiden.js +489 -0
- package/dist/spec/mine/clustering/leiden.js.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.js +98 -0
- package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
- package/dist/spec/mine/generator.d.ts +54 -0
- package/dist/spec/mine/generator.d.ts.map +1 -0
- package/dist/spec/mine/generator.js +267 -0
- package/dist/spec/mine/generator.js.map +1 -0
- package/dist/spec/mine/persist.d.ts +37 -0
- package/dist/spec/mine/persist.d.ts.map +1 -0
- package/dist/spec/mine/persist.js +170 -0
- package/dist/spec/mine/persist.js.map +1 -0
- package/dist/spec/mine/pipeline.d.ts +41 -0
- package/dist/spec/mine/pipeline.d.ts.map +1 -0
- package/dist/spec/mine/pipeline.js +236 -0
- package/dist/spec/mine/pipeline.js.map +1 -0
- package/dist/spec/mine/progress-handler.d.ts +22 -0
- package/dist/spec/mine/progress-handler.d.ts.map +1 -0
- package/dist/spec/mine/progress-handler.js +108 -0
- package/dist/spec/mine/progress-handler.js.map +1 -0
- package/dist/spec/mine/progress.d.ts +23 -0
- package/dist/spec/mine/progress.d.ts.map +1 -0
- package/dist/spec/mine/progress.js +12 -0
- package/dist/spec/mine/progress.js.map +1 -0
- package/dist/spec/mine/scanner.d.ts +59 -0
- package/dist/spec/mine/scanner.d.ts.map +1 -0
- package/dist/spec/mine/scanner.js +383 -0
- package/dist/spec/mine/scanner.js.map +1 -0
- package/dist/spec/types.d.ts +32 -0
- package/dist/spec/types.d.ts.map +1 -1
- package/dist/spec/ui/index.d.ts +9 -0
- package/dist/spec/ui/index.d.ts.map +1 -0
- package/dist/spec/ui/index.js +16 -0
- package/dist/spec/ui/index.js.map +1 -0
- package/dist/spec/ui/progress-handler.d.ts +33 -0
- package/dist/spec/ui/progress-handler.d.ts.map +1 -0
- package/dist/spec/ui/progress-handler.js +132 -0
- package/dist/spec/ui/progress-handler.js.map +1 -0
- package/dist/spec/ui/progress.d.ts +24 -0
- package/dist/spec/ui/progress.d.ts.map +1 -0
- package/dist/spec/ui/progress.js +13 -0
- package/dist/spec/ui/progress.js.map +1 -0
- package/dist/spec/utils/fs.d.ts +45 -0
- package/dist/spec/utils/fs.d.ts.map +1 -0
- package/dist/spec/utils/fs.js +163 -0
- package/dist/spec/utils/fs.js.map +1 -0
- package/dist/spec/utils/index.d.ts +12 -0
- package/dist/spec/utils/index.d.ts.map +1 -0
- package/dist/spec/utils/index.js +26 -0
- package/dist/spec/utils/index.js.map +1 -0
- package/dist/spec/utils/meta.d.ts +32 -0
- package/dist/spec/utils/meta.d.ts.map +1 -0
- package/dist/spec/utils/meta.js +122 -0
- package/dist/spec/utils/meta.js.map +1 -0
- package/dist/spec/utils/truncate.d.ts +86 -0
- package/dist/spec/utils/truncate.d.ts.map +1 -0
- package/dist/spec/utils/truncate.js +172 -0
- package/dist/spec/utils/truncate.js.map +1 -0
- package/dist/spec/utils.d.ts +4 -16
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +6 -58
- package/dist/spec/utils.js.map +1 -1
- package/dist/sync/watcher.d.ts +25 -1
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +76 -3
- package/dist/sync/watcher.js.map +1 -1
- package/dist/telemetry/index.js +3 -3
- package/dist/telemetry/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
|
@@ -17,8 +17,12 @@ exports.resolveMethodOnType = resolveMethodOnType;
|
|
|
17
17
|
exports.matchCppCallChain = matchCppCallChain;
|
|
18
18
|
exports.matchScopedCallChain = matchScopedCallChain;
|
|
19
19
|
exports.matchDottedCallChain = matchDottedCallChain;
|
|
20
|
+
exports.normalizeInferredTypeName = normalizeInferredTypeName;
|
|
21
|
+
exports.clearNameMatcherMemos = clearNameMatcherMemos;
|
|
22
|
+
exports.localReceiverTypePatterns = localReceiverTypePatterns;
|
|
20
23
|
exports.matchMethodCall = matchMethodCall;
|
|
21
24
|
exports.matchFuzzy = matchFuzzy;
|
|
25
|
+
exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
|
|
22
26
|
exports.matchReference = matchReference;
|
|
23
27
|
/**
|
|
24
28
|
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
|
|
@@ -36,11 +40,11 @@ exports.matchReference = matchReference;
|
|
|
36
40
|
* resolution all still run and resolve a ubiquitous name when the context names
|
|
37
41
|
* its exact target. Real repos top out near ~40 same-named methods, so a normal
|
|
38
42
|
* codebase never reaches this; only bulk-vendored code does. Tune via
|
|
39
|
-
* `
|
|
43
|
+
* `HOMEGRAPH_AMBIGUOUS_NAME_CEILING`.
|
|
40
44
|
*/
|
|
41
45
|
const DEFAULT_AMBIGUOUS_NAME_CEILING = 500;
|
|
42
46
|
function resolveAmbiguousNameCeiling() {
|
|
43
|
-
const raw = process.env.
|
|
47
|
+
const raw = process.env.HOMEGRAPH_AMBIGUOUS_NAME_CEILING;
|
|
44
48
|
if (!raw)
|
|
45
49
|
return DEFAULT_AMBIGUOUS_NAME_CEILING;
|
|
46
50
|
const parsed = Number.parseInt(raw, 10);
|
|
@@ -328,6 +332,35 @@ function matchFunctionRef(ref, context) {
|
|
|
328
332
|
}
|
|
329
333
|
return null;
|
|
330
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* A function nested inside another FUNCTION is only callable from within its
|
|
337
|
+
* container — Python, JS/TS, and every closure language scope it lexically.
|
|
338
|
+
* Resolving a bare name from elsewhere to a nested local fabricates an edge
|
|
339
|
+
* scope already rules out: `join(...)` in one function must never bind to a
|
|
340
|
+
* `join` defined inside a DIFFERENT function (#1230). A candidate whose
|
|
341
|
+
* qualifiedName parent is a same-file function/method is kept only when the
|
|
342
|
+
* ref originates inside that parent's line range. Class members are
|
|
343
|
+
* unaffected (their parent resolves to a class-like node), as are top-level
|
|
344
|
+
* symbols and C++ namespace-prefixed names (the prefix has no node).
|
|
345
|
+
*/
|
|
346
|
+
function isLexicallyReachable(candidate, ref, context) {
|
|
347
|
+
if (candidate.kind !== 'function')
|
|
348
|
+
return true;
|
|
349
|
+
const qn = candidate.qualifiedName;
|
|
350
|
+
if (!qn || !qn.includes('::'))
|
|
351
|
+
return true;
|
|
352
|
+
const parentQn = qn.slice(0, qn.lastIndexOf('::'));
|
|
353
|
+
const containers = context
|
|
354
|
+
.getNodesByQualifiedName(parentQn)
|
|
355
|
+
.filter((p) => p.filePath === candidate.filePath &&
|
|
356
|
+
(p.kind === 'function' || p.kind === 'method') &&
|
|
357
|
+
p.startLine <= candidate.startLine &&
|
|
358
|
+
p.endLine >= candidate.endLine);
|
|
359
|
+
if (containers.length === 0)
|
|
360
|
+
return true;
|
|
361
|
+
return (ref.filePath === candidate.filePath &&
|
|
362
|
+
containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine));
|
|
363
|
+
}
|
|
331
364
|
/**
|
|
332
365
|
* Try to resolve a reference by exact name match
|
|
333
366
|
*/
|
|
@@ -342,7 +375,9 @@ function matchByExactName(ref, context) {
|
|
|
342
375
|
// findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
|
|
343
376
|
// large import-heavy (front-end + back-end) repos (#915).
|
|
344
377
|
const candidates = applyLanguageGate(context.getNodesByName(ref.referenceName), ref)
|
|
345
|
-
.filter((n) => n.kind !== 'import')
|
|
378
|
+
.filter((n) => n.kind !== 'import')
|
|
379
|
+
// Nested locals are only reachable from inside their container (#1230).
|
|
380
|
+
.filter((n) => isLexicallyReachable(n, ref, context));
|
|
346
381
|
if (candidates.length === 0) {
|
|
347
382
|
return null;
|
|
348
383
|
}
|
|
@@ -513,11 +548,16 @@ depth = 0) {
|
|
|
513
548
|
// populated in the conformance pass. Still VALIDATED (the method must exist on
|
|
514
549
|
// a supertype), so a wrong inference produces no edge.
|
|
515
550
|
if (depth < 4 && context.getSupertypes) {
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
551
|
+
const viaSupers = nmTimedT('rmot-supers', ref, () => {
|
|
552
|
+
for (const supertype of context.getSupertypes(typeName, ref.language)) {
|
|
553
|
+
const via = resolveMethodOnType(supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1);
|
|
554
|
+
if (via)
|
|
555
|
+
return via;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
});
|
|
559
|
+
if (viaSupers)
|
|
560
|
+
return viaSupers;
|
|
521
561
|
}
|
|
522
562
|
return null;
|
|
523
563
|
}
|
|
@@ -839,10 +879,11 @@ function matchDottedCallChain(ref, context) {
|
|
|
839
879
|
// CRITICAL: resolve the TARGET via a synthetic bare-name ref, but return the
|
|
840
880
|
// match tied to the ORIGINAL `ref` (referenceName `inner().method`). The
|
|
841
881
|
// batched resolver (resolveAndPersistBatched) reads unresolved rows from
|
|
842
|
-
// offset 0 every pass and relies on
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
882
|
+
// offset 0 every pass and relies on the post-batch cleanup (row-id delete
|
|
883
|
+
// for DB-loaded refs, referenceName-keyed delete otherwise, #1269) to
|
|
884
|
+
// clear each resolved row so the batch empties. If we propagated the
|
|
885
|
+
// synthetic ref's bare `method` as `.original`, a key-based delete
|
|
886
|
+
// would never match the stored `inner().method` row, the batch would
|
|
846
887
|
// never drain, and the loop would re-resolve + re-insert forever (a runaway
|
|
847
888
|
// that grew gin's graph to 5M edges / 1.4 GB before this fix).
|
|
848
889
|
const bareRef = { ...ref, referenceName: method };
|
|
@@ -998,7 +1039,50 @@ function normalizeInferredTypeName(raw) {
|
|
|
998
1039
|
* PascalCase is required in the capture where the language convention allows,
|
|
999
1040
|
* as a cheap false-positive guard on top of resolveMethodOnType's validation.
|
|
1000
1041
|
*/
|
|
1042
|
+
/**
|
|
1043
|
+
* Compiled-pattern memo for the receiver-type pattern builders below. They
|
|
1044
|
+
* run for EVERY `receiver.method()` ref the matcher attempts, compiling 2–4
|
|
1045
|
+
* fresh RegExp objects per call — and receivers repeat massively (`self`
|
|
1046
|
+
* alone accounts for tens of thousands of refs on a Lua repo, measured 41µs
|
|
1047
|
+
* per methodCall miss on kong with compilation a large slice). The patterns
|
|
1048
|
+
* are a pure function of (language, receiver) and non-global (`.match()`
|
|
1049
|
+
* never touches lastIndex), so shared instances are behavior-identical.
|
|
1050
|
+
* FIFO-capped with no per-get mutation (the §7a.6 LRU-churn lesson): a hit
|
|
1051
|
+
* costs one Map lookup, overflow evicts oldest, and an evicted entry simply
|
|
1052
|
+
* recompiles exactly as every call did before this memo.
|
|
1053
|
+
*/
|
|
1054
|
+
const PATTERN_MEMO = new Map();
|
|
1055
|
+
const PATTERN_MEMO_CAP = 8192;
|
|
1056
|
+
const INFER_SCAN_STATES = new WeakMap();
|
|
1057
|
+
function getInferScanStates(context) {
|
|
1058
|
+
let m = INFER_SCAN_STATES.get(context);
|
|
1059
|
+
if (!m) {
|
|
1060
|
+
m = new Map();
|
|
1061
|
+
INFER_SCAN_STATES.set(context, m);
|
|
1062
|
+
}
|
|
1063
|
+
return m;
|
|
1064
|
+
}
|
|
1065
|
+
/** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
|
|
1066
|
+
function clearNameMatcherMemos(context) {
|
|
1067
|
+
INFER_SCAN_STATES.delete(context);
|
|
1068
|
+
}
|
|
1069
|
+
function memoPatterns(key, build) {
|
|
1070
|
+
const hit = PATTERN_MEMO.get(key);
|
|
1071
|
+
if (hit)
|
|
1072
|
+
return hit;
|
|
1073
|
+
const patterns = build();
|
|
1074
|
+
if (PATTERN_MEMO.size >= PATTERN_MEMO_CAP) {
|
|
1075
|
+
const oldest = PATTERN_MEMO.keys().next().value;
|
|
1076
|
+
if (oldest !== undefined)
|
|
1077
|
+
PATTERN_MEMO.delete(oldest);
|
|
1078
|
+
}
|
|
1079
|
+
PATTERN_MEMO.set(key, patterns);
|
|
1080
|
+
return patterns;
|
|
1081
|
+
}
|
|
1001
1082
|
function localReceiverTypePatterns(language, r) {
|
|
1083
|
+
return memoPatterns(`${language}|${r}`, () => buildLocalReceiverTypePatterns(language, r));
|
|
1084
|
+
}
|
|
1085
|
+
function buildLocalReceiverTypePatterns(language, r) {
|
|
1002
1086
|
switch (language) {
|
|
1003
1087
|
case 'typescript':
|
|
1004
1088
|
case 'javascript':
|
|
@@ -1241,6 +1325,53 @@ function inferLocalReceiverType(receiverName, ref, context) {
|
|
|
1241
1325
|
}
|
|
1242
1326
|
return null;
|
|
1243
1327
|
};
|
|
1328
|
+
// Incremental-scan memo (INFER_SCAN_STATES): this scan runs for EVERY
|
|
1329
|
+
// `receiver.method()` ref and was measured at 61µs/ref on kong (2.4s of
|
|
1330
|
+
// worker time, 99% misses — `self:` calls hunting a declaration Lua never
|
|
1331
|
+
// writes). Refs for the same (file, scope, receiver) arrive in ~ascending
|
|
1332
|
+
// line order, and the scan is a pure function of the file's immutable
|
|
1333
|
+
// lines, so each line pays its regex matches ONCE per key instead of once
|
|
1334
|
+
// per ref: query(c) = highest matching line in [startIdx..c]; a monotonic
|
|
1335
|
+
// call extends the stored watermark by scanning only (hi..c] (the region
|
|
1336
|
+
// at-or-below the previous answer is already proven empty above it); a
|
|
1337
|
+
// non-monotonic call (rare — refs are rowid-ordered) falls back to the
|
|
1338
|
+
// plain bounded scan and leaves the state alone. componentScoped is keyed
|
|
1339
|
+
// out — its position-independent whole-file sweep below has different
|
|
1340
|
+
// semantics.
|
|
1341
|
+
if (!componentScoped) {
|
|
1342
|
+
const states = getInferScanStates(context);
|
|
1343
|
+
const key = `${ref.filePath}|${startIdx}|${ref.language}|${scanReceiver}`;
|
|
1344
|
+
const state = states.get(key);
|
|
1345
|
+
if (!state) {
|
|
1346
|
+
for (let i = callIdx; i >= startIdx; i--) {
|
|
1347
|
+
const type = matchLine(i);
|
|
1348
|
+
if (type) {
|
|
1349
|
+
states.set(key, { hi: callIdx, ansIdx: i, ansType: type });
|
|
1350
|
+
return type;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
states.set(key, { hi: callIdx, ansIdx: -1, ansType: null });
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
if (callIdx >= state.hi) {
|
|
1357
|
+
for (let i = callIdx; i > state.hi; i--) {
|
|
1358
|
+
const type = matchLine(i);
|
|
1359
|
+
if (type) {
|
|
1360
|
+
state.ansIdx = i;
|
|
1361
|
+
state.ansType = type;
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
state.hi = callIdx;
|
|
1366
|
+
return state.ansIdx >= startIdx ? state.ansType : null;
|
|
1367
|
+
}
|
|
1368
|
+
for (let i = callIdx; i >= startIdx; i--) {
|
|
1369
|
+
const type = matchLine(i);
|
|
1370
|
+
if (type)
|
|
1371
|
+
return type;
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1244
1375
|
// Nearest declaration wins: scan backward from the call to the scope start.
|
|
1245
1376
|
for (let i = callIdx; i >= startIdx; i--) {
|
|
1246
1377
|
const type = matchLine(i);
|
|
@@ -1283,6 +1414,9 @@ function inferLocalReceiverType(receiverName, ref, context) {
|
|
|
1283
1414
|
* shape is handled by inferPhpAssignedPropertyType instead.
|
|
1284
1415
|
*/
|
|
1285
1416
|
function phpPropertyTypePatterns(r) {
|
|
1417
|
+
return memoPatterns(`php-prop|${r}`, () => buildPhpPropertyTypePatterns(r));
|
|
1418
|
+
}
|
|
1419
|
+
function buildPhpPropertyTypePatterns(r) {
|
|
1286
1420
|
return [
|
|
1287
1421
|
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)
|
|
1288
1422
|
new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
|
|
@@ -1362,7 +1496,17 @@ function matchMethodCall(ref, context) {
|
|
|
1362
1496
|
// (with its existing single-candidate / receiver-overlap guards). Without this
|
|
1363
1497
|
// a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
|
|
1364
1498
|
// `Guard.Against.X()`) matched no pattern and never resolved.
|
|
1365
|
-
|
|
1499
|
+
// C++ explicit operator call `a.operator+(b)` reaches the resolver as
|
|
1500
|
+
// `a.operator+` (#1247) — the operator's symbol chars (`+`, `==`, `[]`, `()`)
|
|
1501
|
+
// fail the \w method part of the plain pattern, so admit them explicitly.
|
|
1502
|
+
// Names like `operatorTable` stay on the plain pattern (tried first); the
|
|
1503
|
+
// operator form requires at least one non-word char after `operator`, and
|
|
1504
|
+
// every downstream strategy compares the method part by exact string
|
|
1505
|
+
// equality, so a stray match can't invent an edge.
|
|
1506
|
+
const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/) ??
|
|
1507
|
+
(ref.language === 'cpp'
|
|
1508
|
+
? ref.referenceName.match(/^([\w.]+)\.(operator[^\w\s.]+)$/)
|
|
1509
|
+
: null);
|
|
1366
1510
|
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
|
|
1367
1511
|
// Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
|
|
1368
1512
|
// Recognize these receiver/method separators so local-variable receiver-type
|
|
@@ -1406,9 +1550,9 @@ function matchMethodCall(ref, context) {
|
|
|
1406
1550
|
// shared source-based inferrer. resolveMethodOnType validates the method
|
|
1407
1551
|
// exists on the inferred type, so a mis-inference produces no edge.
|
|
1408
1552
|
if (inferableReceiver) {
|
|
1409
|
-
const inferredType = ref.language === 'cpp'
|
|
1553
|
+
const inferredType = nmTimedT('mc-infer', ref, () => ref.language === 'cpp'
|
|
1410
1554
|
? inferCppReceiverType(objectOrClass, ref, context)
|
|
1411
|
-
: inferLocalReceiverType(objectOrClass, ref, context);
|
|
1555
|
+
: inferLocalReceiverType(objectOrClass, ref, context));
|
|
1412
1556
|
if (inferredType) {
|
|
1413
1557
|
// Java/Kotlin: when two classes share the simple name, the file's import
|
|
1414
1558
|
// pins WHICH one (#314). Other languages disambiguate by call-site file.
|
|
@@ -1417,12 +1561,26 @@ function matchMethodCall(ref, context) {
|
|
|
1417
1561
|
.getImportMappings(ref.filePath, ref.language)
|
|
1418
1562
|
.find((i) => i.localName === inferredType)?.source
|
|
1419
1563
|
: undefined;
|
|
1420
|
-
const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
|
|
1564
|
+
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
|
|
1421
1565
|
if (typedMatch) {
|
|
1422
1566
|
return typedMatch;
|
|
1423
1567
|
}
|
|
1424
1568
|
}
|
|
1425
1569
|
}
|
|
1570
|
+
// Go 2-hop field chain `base.field.Method` (#1276): the base's type comes
|
|
1571
|
+
// from the enclosing scope (typed parameter / method receiver / local var),
|
|
1572
|
+
// the field's declared type from that struct's own declaration lines, and
|
|
1573
|
+
// the method is VALIDATED on the field's type by resolveMethodOnType. This
|
|
1574
|
+
// branch is EXCLUSIVE for chained Go receivers: when the hop can't be
|
|
1575
|
+
// inferred or the field's type is external (`conn *sql.DB` — no project
|
|
1576
|
+
// node), the ref stays unresolved rather than falling through to the
|
|
1577
|
+
// bare-name strategies below, which is exactly how `target.conn.Exec(...)`
|
|
1578
|
+
// fabricated a dependency on an unrelated local interface's same-named
|
|
1579
|
+
// method. Chained Go receivers were never emitted before #1276, so there
|
|
1580
|
+
// is no prior recall to preserve on the fallback path.
|
|
1581
|
+
if (ref.language === 'go' && dotMatch && objectOrClass.includes('.')) {
|
|
1582
|
+
return matchGoFieldChainCall(objectOrClass, methodName, ref, context);
|
|
1583
|
+
}
|
|
1426
1584
|
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
|
|
1427
1585
|
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
|
|
1428
1586
|
// the field in the enclosing class to get its declared type, then resolve
|
|
@@ -1436,7 +1594,7 @@ function matchMethodCall(ref, context) {
|
|
|
1436
1594
|
// imported FQN so resolveMethodOnType can disambiguate (#314).
|
|
1437
1595
|
const imports = context.getImportMappings(ref.filePath, ref.language);
|
|
1438
1596
|
const importedFqn = imports.find((i) => i.localName === inferredType)?.source;
|
|
1439
|
-
const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
|
|
1597
|
+
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
|
|
1440
1598
|
if (typedMatch) {
|
|
1441
1599
|
return typedMatch;
|
|
1442
1600
|
}
|
|
@@ -1447,33 +1605,23 @@ function matchMethodCall(ref, context) {
|
|
|
1447
1605
|
// with a `Logger` in both `a/` and `b/`), try the class in the call site's
|
|
1448
1606
|
// own file first — otherwise the first-indexed class wins and a call in `b/`
|
|
1449
1607
|
// resolves to `a/`'s method (#1079).
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
return {
|
|
1462
|
-
original: ref,
|
|
1463
|
-
targetNodeId: methodNode.id,
|
|
1464
|
-
confidence: 0.85,
|
|
1465
|
-
resolvedBy: 'qualified-name',
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1608
|
+
const strat1 = nmTimedT('mc-class', ref, () => {
|
|
1609
|
+
// ArkTS: same-file OR imported local name only. Unrestricted project-wide
|
|
1610
|
+
// matches welded every CFG `Foo.getInstance` seed onto one method; requiring
|
|
1611
|
+
// `import { Foo }` (or a same-file class) keeps cross-module `Lib.use` while
|
|
1612
|
+
// dropping ambient / unimported stubs.
|
|
1613
|
+
let classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
|
|
1614
|
+
if (ref.language === 'arkts') {
|
|
1615
|
+
const imported = context
|
|
1616
|
+
.getImportMappings(ref.filePath, ref.language)
|
|
1617
|
+
.some((i) => i.localName === objectOrClass);
|
|
1618
|
+
classCandidates = classCandidates.filter((n) => n.filePath === ref.filePath || imported);
|
|
1468
1619
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
|
|
1475
|
-
for (const classNode of fuzzyClassCandidates) {
|
|
1476
|
-
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
|
1620
|
+
for (const classNode of classCandidates) {
|
|
1621
|
+
if (classNode.kind === 'class' ||
|
|
1622
|
+
classNode.kind === 'struct' ||
|
|
1623
|
+
classNode.kind === 'interface' ||
|
|
1624
|
+
(ref.language === 'arkts' && classNode.kind === 'component')) {
|
|
1477
1625
|
// Skip cross-language class matches
|
|
1478
1626
|
if (classNode.language !== ref.language)
|
|
1479
1627
|
continue;
|
|
@@ -1485,67 +1633,192 @@ function matchMethodCall(ref, context) {
|
|
|
1485
1633
|
return {
|
|
1486
1634
|
original: ref,
|
|
1487
1635
|
targetNodeId: methodNode.id,
|
|
1488
|
-
confidence: 0.
|
|
1489
|
-
resolvedBy: '
|
|
1636
|
+
confidence: 0.85,
|
|
1637
|
+
resolvedBy: 'qualified-name',
|
|
1490
1638
|
};
|
|
1491
1639
|
}
|
|
1492
1640
|
}
|
|
1493
1641
|
}
|
|
1642
|
+
return null;
|
|
1643
|
+
});
|
|
1644
|
+
if (strat1)
|
|
1645
|
+
return strat1;
|
|
1646
|
+
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
|
1647
|
+
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
|
1648
|
+
const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
|
|
1649
|
+
// ArkTS: skip — cross-module seeds are already `Class.method`; capitalizing
|
|
1650
|
+
// locals caused wrong bindings on large Harmony trees.
|
|
1651
|
+
if (ref.language !== 'arkts' && capitalizedReceiver !== objectOrClass) {
|
|
1652
|
+
const strat2 = nmTimedT('mc-capital', ref, () => {
|
|
1653
|
+
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
|
|
1654
|
+
for (const classNode of fuzzyClassCandidates) {
|
|
1655
|
+
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
|
1656
|
+
// Skip cross-language class matches
|
|
1657
|
+
if (classNode.language !== ref.language)
|
|
1658
|
+
continue;
|
|
1659
|
+
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
1660
|
+
const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
|
|
1661
|
+
n.name === methodName &&
|
|
1662
|
+
n.qualifiedName.includes(classNode.name));
|
|
1663
|
+
if (methodNode) {
|
|
1664
|
+
return {
|
|
1665
|
+
original: ref,
|
|
1666
|
+
targetNodeId: methodNode.id,
|
|
1667
|
+
confidence: 0.8,
|
|
1668
|
+
resolvedBy: 'instance-method',
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return null;
|
|
1674
|
+
});
|
|
1675
|
+
if (strat2)
|
|
1676
|
+
return strat2;
|
|
1494
1677
|
}
|
|
1495
1678
|
// Strategy 3: Find methods by name across the codebase, match by receiver
|
|
1496
1679
|
// name similarity with the containing class. Handles abbreviated variable
|
|
1497
1680
|
// names like permissionEngine → PermissionRuleEngine.
|
|
1681
|
+
// ArkTS: never — unique/score fallthrough turns `Foo.getInstance` into a
|
|
1682
|
+
// project-wide getInstance magnet when Class is wrong/missing.
|
|
1683
|
+
if (ref.language === 'arkts') {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1498
1686
|
if (methodName) {
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
|
|
1510
|
-
// Filter to same-language candidates first
|
|
1511
|
-
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
|
1512
|
-
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
|
1513
|
-
// If only one same-language method with this name exists, use it
|
|
1514
|
-
if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
|
|
1515
|
-
return {
|
|
1516
|
-
original: ref,
|
|
1517
|
-
targetNodeId: targetMethods[0].id,
|
|
1518
|
-
confidence: 0.7,
|
|
1519
|
-
resolvedBy: 'instance-method',
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
// Multiple methods: score by receiver name word overlap with class name
|
|
1523
|
-
if (targetMethods.length > 1) {
|
|
1524
|
-
const receiverWords = splitCamelCase(objectOrClass);
|
|
1525
|
-
let bestMatch;
|
|
1526
|
-
let bestScore = 0;
|
|
1527
|
-
// Same-file candidates first, so a score tie (`score > bestScore` keeps
|
|
1528
|
-
// the first seen) resolves to the call site's own file rather than the
|
|
1529
|
-
// first-indexed duplicate (#1079).
|
|
1530
|
-
for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
|
|
1531
|
-
const classWords = splitCamelCase(method.qualifiedName);
|
|
1532
|
-
let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
|
|
1533
|
-
// Bonus for same language
|
|
1534
|
-
if (method.language === ref.language)
|
|
1535
|
-
score += 1;
|
|
1536
|
-
if (score > bestScore) {
|
|
1537
|
-
bestScore = score;
|
|
1538
|
-
bestMatch = method;
|
|
1539
|
-
}
|
|
1687
|
+
const strat3 = nmTimedT('mc-byname', ref, () => {
|
|
1688
|
+
const methodCandidates = context.getNodesByName(methodName);
|
|
1689
|
+
// Ubiquitous-method ceiling (#999): a method name re-declared across a
|
|
1690
|
+
// vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
|
|
1691
|
+
// K candidates that receiver-word overlap can't reliably disambiguate —
|
|
1692
|
+
// and filtering + scoring all K per call is the O(K²) cost that wedged
|
|
1693
|
+
// "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
|
|
1694
|
+
// (class-name match) already had their precise shot above.
|
|
1695
|
+
if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
|
|
1696
|
+
return null;
|
|
1540
1697
|
}
|
|
1541
|
-
|
|
1698
|
+
const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
|
|
1699
|
+
// Filter to same-language candidates first
|
|
1700
|
+
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
|
1701
|
+
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
|
1702
|
+
// If only one same-language method with this name exists, use it
|
|
1703
|
+
if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
|
|
1542
1704
|
return {
|
|
1543
1705
|
original: ref,
|
|
1544
|
-
targetNodeId:
|
|
1545
|
-
confidence: 0.
|
|
1706
|
+
targetNodeId: targetMethods[0].id,
|
|
1707
|
+
confidence: 0.7,
|
|
1546
1708
|
resolvedBy: 'instance-method',
|
|
1547
1709
|
};
|
|
1548
1710
|
}
|
|
1711
|
+
// Multiple methods: score by receiver name word overlap with class name
|
|
1712
|
+
if (targetMethods.length > 1) {
|
|
1713
|
+
const receiverWords = splitCamelCase(objectOrClass);
|
|
1714
|
+
let bestMatch;
|
|
1715
|
+
let bestScore = 0;
|
|
1716
|
+
// Same-file candidates first, so a score tie (`score > bestScore` keeps
|
|
1717
|
+
// the first seen) resolves to the call site's own file rather than the
|
|
1718
|
+
// first-indexed duplicate (#1079).
|
|
1719
|
+
for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
|
|
1720
|
+
const classWords = splitCamelCase(method.qualifiedName);
|
|
1721
|
+
let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
|
|
1722
|
+
// Bonus for same language
|
|
1723
|
+
if (method.language === ref.language)
|
|
1724
|
+
score += 1;
|
|
1725
|
+
if (score > bestScore) {
|
|
1726
|
+
bestScore = score;
|
|
1727
|
+
bestMatch = method;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (bestMatch && bestScore >= 2) {
|
|
1731
|
+
return {
|
|
1732
|
+
original: ref,
|
|
1733
|
+
targetNodeId: bestMatch.id,
|
|
1734
|
+
confidence: 0.65,
|
|
1735
|
+
resolvedBy: 'instance-method',
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return null;
|
|
1740
|
+
});
|
|
1741
|
+
if (strat3)
|
|
1742
|
+
return strat3;
|
|
1743
|
+
}
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
/** Go builtin/primitive field types that can never carry a project method. */
|
|
1747
|
+
const GO_BUILTIN_FIELD_TYPES = new Set([
|
|
1748
|
+
'string', 'bool', 'byte', 'rune', 'error', 'any',
|
|
1749
|
+
'int', 'int8', 'int16', 'int32', 'int64',
|
|
1750
|
+
'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
|
|
1751
|
+
'float32', 'float64', 'complex64', 'complex128',
|
|
1752
|
+
'chan', 'map', 'func', 'struct', 'interface',
|
|
1753
|
+
]);
|
|
1754
|
+
/**
|
|
1755
|
+
* Resolve a Go 2-hop field-chain call `base.field.Method(...)` (#1276):
|
|
1756
|
+
* `target.conn.Exec("insert")` where `func (target *Target) Write()` and
|
|
1757
|
+
* `type Target struct { conn *sql.DB }`. Two inference hops, both read from
|
|
1758
|
+
* source the same way #1108 does:
|
|
1759
|
+
* 1. `base`'s type from the enclosing scope (method receiver, typed
|
|
1760
|
+
* parameter, or local declaration) via inferLocalReceiverType;
|
|
1761
|
+
* 2. `field`'s declared type from the struct's own declaration lines.
|
|
1762
|
+
* The method is then resolved AND VALIDATED on the field's type. A field
|
|
1763
|
+
* whose type has no project node (`sql.DB`, any external dependency) yields
|
|
1764
|
+
* null — the caller treats this branch as exclusive for chained Go
|
|
1765
|
+
* receivers, so the ref stays unresolved instead of name-guessing.
|
|
1766
|
+
*/
|
|
1767
|
+
function matchGoFieldChainCall(receiverChain, methodName, ref, context) {
|
|
1768
|
+
const segs = receiverChain.split('.');
|
|
1769
|
+
if (segs.length !== 2 || !segs[0] || !segs[1])
|
|
1770
|
+
return null;
|
|
1771
|
+
const [base, field] = segs;
|
|
1772
|
+
const baseType = inferLocalReceiverType(base, ref, context);
|
|
1773
|
+
if (!baseType)
|
|
1774
|
+
return null;
|
|
1775
|
+
const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1776
|
+
const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`);
|
|
1777
|
+
const structs = preferCallSiteFile(context.getNodesByName(baseType), ref.filePath).filter((n) => (n.kind === 'struct' || n.kind === 'class') && n.language === 'go');
|
|
1778
|
+
for (const s of structs) {
|
|
1779
|
+
const source = context.readFile(s.filePath);
|
|
1780
|
+
if (!source)
|
|
1781
|
+
continue;
|
|
1782
|
+
// Only the struct's own declaration lines — a same-named identifier
|
|
1783
|
+
// elsewhere in the file can't donate a type. Matched LINE BY LINE with
|
|
1784
|
+
// comments stripped: chi's `Mux` has a doc comment reading "the tree
|
|
1785
|
+
// router" right above `tree *node`, and a whole-block match captured
|
|
1786
|
+
// `router` from the prose instead of `node` from the field.
|
|
1787
|
+
const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
|
|
1788
|
+
for (const rawLine of declLines) {
|
|
1789
|
+
const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
|
|
1790
|
+
const m = line.match(fieldTypeRe);
|
|
1791
|
+
if (!m || !m[1])
|
|
1792
|
+
continue;
|
|
1793
|
+
const rawType = m[1];
|
|
1794
|
+
// A package-qualified field type (`http.Handler`, `sql.DB`) is only
|
|
1795
|
+
// followed when the package is IN-MODULE: stripping the qualifier and
|
|
1796
|
+
// matching the bare name would conflate a stdlib/third-party type with
|
|
1797
|
+
// any same-named project type — on chi, `handler http.Handler` bound
|
|
1798
|
+
// to an example app's unrelated local `Handler`. That is the exact
|
|
1799
|
+
// fabrication this matcher exists to prevent (#1276).
|
|
1800
|
+
if (rawType.includes('.')) {
|
|
1801
|
+
const pkg = rawType.split('.')[0];
|
|
1802
|
+
const mod = context.getGoModule?.();
|
|
1803
|
+
const imp = context
|
|
1804
|
+
.getImportMappings(s.filePath, 'go')
|
|
1805
|
+
.find((i) => i.localName === pkg);
|
|
1806
|
+
const inModule = !!mod &&
|
|
1807
|
+
!!imp &&
|
|
1808
|
+
(imp.source === mod.modulePath || imp.source.startsWith(mod.modulePath + '/'));
|
|
1809
|
+
if (!inModule)
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
// Unexported (lowercase) types are idiomatic Go and stay eligible —
|
|
1813
|
+
// chi's `mx.tree.FindRoute()` chains through `tree *node`. A
|
|
1814
|
+
// mis-capture is harmless: resolveMethodOnType only returns a
|
|
1815
|
+
// validated `<type>::<method>` match.
|
|
1816
|
+
const fieldType = rawType.split('.').pop();
|
|
1817
|
+
if (!fieldType || !/^[A-Za-z_]/.test(fieldType) || GO_BUILTIN_FIELD_TYPES.has(fieldType))
|
|
1818
|
+
continue;
|
|
1819
|
+
const resolved = resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
|
|
1820
|
+
if (resolved)
|
|
1821
|
+
return resolved;
|
|
1549
1822
|
}
|
|
1550
1823
|
}
|
|
1551
1824
|
return null;
|
|
@@ -1708,6 +1981,49 @@ function matchFuzzy(ref, context) {
|
|
|
1708
1981
|
*/
|
|
1709
1982
|
/** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
|
|
1710
1983
|
const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
|
|
1984
|
+
/**
|
|
1985
|
+
* HOMEGRAPH_RESOLVE_PROFILE=2 sub-stage attribution for matchReference's
|
|
1986
|
+
* strategy pipeline (`nm:<stage>|<refKind>|hit/miss`). Module-global because
|
|
1987
|
+
* the matcher is a free function; each thread (main + every pool worker) has
|
|
1988
|
+
* its own module instance, and dumpNameMatcherProfile is invoked from
|
|
1989
|
+
* ReferenceResolver.dumpResolveProfile so worker tables surface too.
|
|
1990
|
+
* (CODEGRAPH_RESOLVE_PROFILE still accepted for upstream-compat.)
|
|
1991
|
+
*/
|
|
1992
|
+
const NM_PROFILE = process.env.HOMEGRAPH_RESOLVE_PROFILE === '2' ||
|
|
1993
|
+
process.env.CODEGRAPH_RESOLVE_PROFILE === '2'
|
|
1994
|
+
? new Map()
|
|
1995
|
+
: null;
|
|
1996
|
+
function nmTimedT(stage, ref, fn) {
|
|
1997
|
+
if (!NM_PROFILE)
|
|
1998
|
+
return fn();
|
|
1999
|
+
const t0 = process.hrtime.bigint();
|
|
2000
|
+
const r = fn();
|
|
2001
|
+
const dt = process.hrtime.bigint() - t0;
|
|
2002
|
+
const key = `nm:${stage}|${ref.referenceKind}|${r ? 'hit' : 'miss'}`;
|
|
2003
|
+
const slot = NM_PROFILE.get(key);
|
|
2004
|
+
if (slot) {
|
|
2005
|
+
slot.n++;
|
|
2006
|
+
slot.ns += dt;
|
|
2007
|
+
}
|
|
2008
|
+
else {
|
|
2009
|
+
NM_PROFILE.set(key, { n: 1, ns: dt });
|
|
2010
|
+
}
|
|
2011
|
+
return r;
|
|
2012
|
+
}
|
|
2013
|
+
function nmTimed(stage, ref, fn) {
|
|
2014
|
+
return nmTimedT(stage, ref, fn);
|
|
2015
|
+
}
|
|
2016
|
+
/** Dump this thread's matchReference sub-stage table to stderr (no-op unless =2). */
|
|
2017
|
+
function dumpNameMatcherProfile(label) {
|
|
2018
|
+
if (!NM_PROFILE || NM_PROFILE.size === 0)
|
|
2019
|
+
return;
|
|
2020
|
+
const rows = [...NM_PROFILE.entries()]
|
|
2021
|
+
.map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1000000n) }))
|
|
2022
|
+
.sort((a, b) => b.ms - a.ms);
|
|
2023
|
+
for (const r of rows) {
|
|
2024
|
+
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`);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1711
2027
|
function matchReference(ref, context) {
|
|
1712
2028
|
// Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
|
|
1713
2029
|
// never the fuzzy/qualified fallthrough below (a wrong callback edge is
|
|
@@ -1769,18 +2085,18 @@ function matchReference(ref, context) {
|
|
|
1769
2085
|
// Try strategies in order of confidence
|
|
1770
2086
|
let result;
|
|
1771
2087
|
// 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
|
|
1772
|
-
result = matchByFilePath(ref, context);
|
|
2088
|
+
result = nmTimed('filePath', ref, () => matchByFilePath(ref, context));
|
|
1773
2089
|
if (result)
|
|
1774
2090
|
return result;
|
|
1775
2091
|
// 1. Qualified name match (highest confidence)
|
|
1776
|
-
result = matchByQualifiedName(ref, context);
|
|
2092
|
+
result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context));
|
|
1777
2093
|
if (result)
|
|
1778
2094
|
return result;
|
|
1779
2095
|
// 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
|
|
1780
2096
|
// encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
|
|
1781
2097
|
// receiver's type from what the inner call returns, then the method on it.
|
|
1782
2098
|
if (ref.language === 'cpp' || ref.language === 'c') {
|
|
1783
|
-
result = matchCppCallChain(ref, context);
|
|
2099
|
+
result = nmTimed('cppChain', ref, () => matchCppCallChain(ref, context));
|
|
1784
2100
|
if (result)
|
|
1785
2101
|
return result;
|
|
1786
2102
|
}
|
|
@@ -1789,7 +2105,7 @@ function matchReference(ref, context) {
|
|
|
1789
2105
|
// type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
|
|
1790
2106
|
// concrete return type.
|
|
1791
2107
|
if (ref.language === 'php' || ref.language === 'rust') {
|
|
1792
|
-
result = matchScopedCallChain(ref, context);
|
|
2108
|
+
result = nmTimed('scopedChain', ref, () => matchScopedCallChain(ref, context));
|
|
1793
2109
|
if (result)
|
|
1794
2110
|
return result;
|
|
1795
2111
|
}
|
|
@@ -1809,20 +2125,46 @@ function matchReference(ref, context) {
|
|
|
1809
2125
|
ref.language === 'dart' ||
|
|
1810
2126
|
ref.language === 'objc' ||
|
|
1811
2127
|
ref.language === 'pascal') {
|
|
1812
|
-
result = matchDottedCallChain(ref, context);
|
|
2128
|
+
result = nmTimed('dottedChain', ref, () => matchDottedCallChain(ref, context));
|
|
1813
2129
|
if (result)
|
|
1814
2130
|
return result;
|
|
1815
2131
|
}
|
|
1816
2132
|
// 2. Method call pattern
|
|
1817
|
-
result = matchMethodCall(ref, context);
|
|
2133
|
+
result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
|
|
1818
2134
|
if (result)
|
|
1819
2135
|
return result;
|
|
2136
|
+
// ArkTS calls / instantiates: no bare exact-match or fuzzy. Cross-module
|
|
2137
|
+
// seeds are `Class.method` (handled above) or a class name for `new`
|
|
2138
|
+
// (`instantiates`). Falling through previously linked every `when`/`toString`
|
|
2139
|
+
// invoke to a single same-named symbol (100k+ false edges on scene_board).
|
|
2140
|
+
if (ref.language === 'arkts' &&
|
|
2141
|
+
(ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates')) {
|
|
2142
|
+
if (ref.referenceKind === 'instantiates' && !ref.referenceName.includes('.')) {
|
|
2143
|
+
// Same-file only — project-wide unique class still over-links constructor
|
|
2144
|
+
// seeds (`GridOccupyStatus` × thousands). Cross-module `new Foo` stays
|
|
2145
|
+
// unresolved rather than guessed (import path is also disabled above).
|
|
2146
|
+
const sameFile = context
|
|
2147
|
+
.getNodesByName(ref.referenceName)
|
|
2148
|
+
.filter((n) => n.language === 'arkts' &&
|
|
2149
|
+
n.filePath === ref.filePath &&
|
|
2150
|
+
(n.kind === 'class' || n.kind === 'struct' || n.kind === 'component'));
|
|
2151
|
+
if (sameFile.length === 1) {
|
|
2152
|
+
return {
|
|
2153
|
+
original: ref,
|
|
2154
|
+
targetNodeId: sameFile[0].id,
|
|
2155
|
+
confidence: 0.85,
|
|
2156
|
+
resolvedBy: 'exact-match',
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
1820
2162
|
// 3. Exact name match
|
|
1821
|
-
result = matchByExactName(ref, context);
|
|
2163
|
+
result = nmTimed('exactName', ref, () => matchByExactName(ref, context));
|
|
1822
2164
|
if (result)
|
|
1823
2165
|
return result;
|
|
1824
2166
|
// 4. Fuzzy match (lowest confidence)
|
|
1825
|
-
result = matchFuzzy(ref, context);
|
|
2167
|
+
result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
|
|
1826
2168
|
if (result)
|
|
1827
2169
|
return result;
|
|
1828
2170
|
return null;
|