gitnexus 1.6.10-rc.2 → 1.6.10-rc.21
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 +56 -12
- package/dist/cli/analyze.js +166 -5
- package/dist/cli/clean.js +17 -3
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/doctor.d.ts +18 -0
- package/dist/cli/doctor.js +76 -3
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/help-i18n.js +4 -0
- package/dist/cli/i18n/en.d.ts +10 -5
- package/dist/cli/i18n/en.js +10 -5
- package/dist/cli/i18n/resources.d.ts +15 -5
- package/dist/cli/i18n/zh-CN.d.ts +5 -0
- package/dist/cli/i18n/zh-CN.js +10 -5
- package/dist/cli/index.js +12 -1
- package/dist/core/augmentation/engine.js +4 -3
- package/dist/core/embeddings/embedder.js +16 -2
- package/dist/core/embeddings/embedding-pipeline.d.ts +20 -0
- package/dist/core/embeddings/embedding-pipeline.js +10 -2
- package/dist/core/embeddings/http-client.d.ts +28 -1
- package/dist/core/embeddings/http-client.js +107 -15
- package/dist/core/embeddings/node-module-compat.d.ts +23 -0
- package/dist/core/embeddings/node-module-compat.js +23 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
- package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
- package/dist/core/embeddings/runtime-install.d.ts +119 -0
- package/dist/core/embeddings/runtime-install.js +372 -0
- package/dist/core/embeddings/runtime-support.d.ts +48 -17
- package/dist/core/embeddings/runtime-support.js +96 -0
- package/dist/core/group/extractors/http-patterns/python.js +246 -108
- package/dist/core/group/extractors/manifest-extractor.js +4 -0
- package/dist/core/incremental/escalation-gate.d.ts +38 -0
- package/dist/core/incremental/escalation-gate.js +48 -0
- package/dist/core/incremental/shadow-candidates.d.ts +3 -3
- package/dist/core/incremental/shadow-candidates.js +3 -3
- package/dist/core/incremental/subgraph-extract.d.ts +2 -2
- package/dist/core/incremental/subgraph-extract.js +2 -2
- package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
- package/dist/core/ingestion/cobol-processor.js +27 -26
- package/dist/core/ingestion/community-processor.d.ts +51 -1
- package/dist/core/ingestion/community-processor.js +316 -63
- package/dist/core/ingestion/emit-references.js +3 -2
- package/dist/core/ingestion/languages/cpp/captures.js +157 -74
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +12 -1
- package/dist/core/ingestion/utils/line-base.d.ts +20 -0
- package/dist/core/ingestion/utils/line-base.js +20 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
- package/dist/core/ingestion/utils/symbol-labels.js +45 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
- package/dist/core/ingestion/workers/parse-worker.js +40 -5
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +14 -0
- package/dist/core/ingestion/workers/worker-pool.js +85 -5
- package/dist/core/lbug/csv-generator.js +8 -2
- package/dist/core/lbug/cypher-escape.d.ts +20 -0
- package/dist/core/lbug/cypher-escape.js +20 -0
- package/dist/core/lbug/extension-load-error.d.ts +67 -0
- package/dist/core/lbug/extension-load-error.js +320 -0
- package/dist/core/lbug/extension-loader.d.ts +17 -2
- package/dist/core/lbug/extension-loader.js +38 -13
- package/dist/core/lbug/lbug-adapter.d.ts +147 -1
- package/dist/core/lbug/lbug-adapter.js +390 -70
- package/dist/core/lbug/lbug-config.d.ts +28 -0
- package/dist/core/lbug/lbug-config.js +114 -4
- package/dist/core/lbug/native-check.d.ts +22 -0
- package/dist/core/lbug/native-check.js +66 -0
- package/dist/core/lbug/pool-adapter.js +10 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +172 -0
- package/dist/core/lbug/sidecar-recovery.js +418 -16
- package/dist/core/platform/capabilities.js +27 -1
- package/dist/core/run-analyze.js +470 -108
- package/dist/core/search/fts-indexes.d.ts +7 -0
- package/dist/core/search/fts-indexes.js +37 -0
- package/dist/core/tree-sitter/safe-parse.d.ts +8 -3
- package/dist/core/tree-sitter/safe-parse.js +21 -7
- package/dist/core/wiki/graph-queries.js +6 -5
- package/dist/mcp/core/embedder.js +16 -2
- package/dist/mcp/local/line-display.d.ts +22 -0
- package/dist/mcp/local/line-display.js +3 -0
- package/dist/mcp/local/local-backend.d.ts +14 -0
- package/dist/mcp/local/local-backend.js +63 -19
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server/analyze-launch.d.ts +6 -0
- package/dist/server/analyze-launch.js +82 -6
- package/dist/server/api.d.ts +30 -0
- package/dist/server/api.js +63 -21
- package/dist/storage/parse-cache.js +1 -1
- package/dist/storage/repo-manager.d.ts +65 -1
- package/dist/storage/repo-manager.js +18 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/package.json +5 -3
- package/scripts/cross-platform-tests.ts +43 -0
- package/scripts/ensure-fts.ts +32 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +46 -6
- package/scripts/shard-arg.ts +30 -0
- package/web/assets/ProcessFlowModal-Dna7GkwO.js +22 -0
- package/web/assets/{agent-Dli_x7bU.js → agent-DNYDy-KA.js} +49 -49
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-BU5ZRITg.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-BkWhpzWB.js → blockDiagram-7IZFK4PR-Dut1BpTV.js} +2 -2
- package/web/assets/{c4Diagram-Y2BXMSZH-DtBvpzWD.js → c4Diagram-Y2BXMSZH-DOXVs7-C.js} +2 -2
- package/web/assets/chunk-2T2R6R2M-pkw2oXHH.js +4 -0
- package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +1 -0
- package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +321 -0
- package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +159 -0
- package/web/assets/{chunk-6764PJDD-DpIS7hxD.js → chunk-6764PJDD-C7xxk063.js} +1 -1
- package/web/assets/{chunk-67TQ5CYL-4sNPJU1q.js → chunk-67TQ5CYL-zxhDWgyF.js} +3 -3
- package/web/assets/chunk-7FYTHRHK-CsmYIswB.js +32 -0
- package/web/assets/chunk-7J6CGLKN-AmkN8jJN.js +10 -0
- package/web/assets/chunk-7W6UQGC5-pyKGssV3.js +1 -0
- package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +1 -0
- package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +1 -0
- package/web/assets/{chunk-INKRHTLW-Nu4ri9P-.js → chunk-INKRHTLW-D9BM9HWZ.js} +1 -1
- package/web/assets/chunk-J5EP6P6S-L19oz55R.js +1 -0
- package/web/assets/{chunk-KGFNY3KK-JGMJvKCK.js → chunk-KGFNY3KK-tD2F6wxK.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-zm6n242I.js +1 -0
- package/web/assets/{chunk-KRXBNO2N-BW5o0KUN.js → chunk-KRXBNO2N-C-UNuKKf.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-CfLLCywY.js → chunk-LCXTWHL2-Ba9JK-6F.js} +2 -2
- package/web/assets/{chunk-LII3EMHJ-D-jm-dLa.js → chunk-LII3EMHJ-EGx-eBIu.js} +1 -1
- package/web/assets/chunk-LRIF4GLE-C3_PqUBq.js +1 -0
- package/web/assets/{chunk-QA3QBVWF-BgRW3SXd.js → chunk-QA3QBVWF-D4cSJBf6.js} +1 -1
- package/web/assets/chunk-RERM46MO-B5dAdWpP.js +1 -0
- package/web/assets/{chunk-RG4AUYOV-CqWzWsV3.js → chunk-RG4AUYOV-OqBBMwuQ.js} +4 -4
- package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +1 -0
- package/web/assets/chunk-RLI5ZMPA-CkXZ0y0U.js +1 -0
- package/web/assets/{chunk-T2UQINTJ-DSBqkzH7.js → chunk-T2UQINTJ-BJusEiGe.js} +1 -1
- package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +1 -0
- package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +1 -0
- package/web/assets/{chunk-UY5QBCOK-DyF0vyGd.js → chunk-UY5QBCOK-BCpUlARQ.js} +1 -1
- package/web/assets/chunk-VU6ZFW4Y-BeXVk45_.js +2 -0
- package/web/assets/{chunk-W44A43WB-JqMLIpR0.js → chunk-W44A43WB-BN8EQc_u.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CxN8oiwI.js → chunk-ZXARS5L4-BI_qR2Nm.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-D9zSIg0x.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-D9zSIg0x.js +1 -0
- package/web/assets/cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js +1 -0
- package/web/assets/dagre-ND4H6XIP-CpcV5kbl.js +4 -0
- package/web/assets/diagram-3NCE3AQN-6SMxBRpj.js +43 -0
- package/web/assets/diagram-GF46GFSD-BZEdNcRZ.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-DFCfd5LI.js → diagram-HNR7UZ2L-DxKW2a2Y.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-HfTHcXX_.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-CyijUS8r.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-8X1y6R4Y.js → erDiagram-L5TCEMPS-D0fvpWEX.js} +3 -3
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +1 -0
- package/web/assets/flowDiagram-H6V6AXG4-Bvr9bbhk.js +162 -0
- package/web/assets/{ganttDiagram-JCBTUEKG-D3iL2Aet.js → ganttDiagram-JCBTUEKG-TzODrLzv.js} +2 -2
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-ONQSwJIY.js +106 -0
- package/web/assets/index-B4eB4dNZ.js +575 -0
- package/web/assets/index-CX_fADmQ.css +2 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-DIPVUIQ6.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-DfYpxnE5.js → ishikawaDiagram-BNXS4ZKH-BLjivjXL.js} +3 -3
- package/web/assets/{journeyDiagram-M6C3CM3L-CvArszLo.js → journeyDiagram-M6C3CM3L-Cetgs3NH.js} +3 -3
- package/web/assets/{kanban-definition-75IXJCU3-CpfVLLf9.js → kanban-definition-75IXJCU3-DMlCaIpf.js} +5 -5
- package/web/assets/{katex-K3KEBU37-CTc5BslQ.js → katex-K3KEBU37-qRQBy-EZ.js} +2 -2
- package/web/assets/{mindmap-definition-2TDM6QVE-DypvbOMQ.js → mindmap-definition-2TDM6QVE-B4GbXQBc.js} +3 -3
- package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +1 -0
- package/web/assets/{pieDiagram-CU6KROY3-s27EFWgP.js → pieDiagram-CU6KROY3-D3YC_xyg.js} +2 -2
- package/web/assets/{quadrantDiagram-VICAPDV7-yXSZ2lLz.js → quadrantDiagram-VICAPDV7-B1TQnlm0.js} +2 -2
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-CWhqInuf.js → requirementDiagram-JXO7QTGE-f8wczLQf.js} +3 -3
- package/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/assets/{sankeyDiagram-URQDO5SZ-1qEjlFIB.js → sankeyDiagram-URQDO5SZ-BTxzG5j0.js} +2 -2
- package/web/assets/{sequenceDiagram-VS2MUI6T-RyTlZ5cR.js → sequenceDiagram-VS2MUI6T-DuamM-IR.js} +5 -5
- package/web/assets/src-BNCXICdA.js +1 -0
- package/web/assets/stateDiagram-7D4R322I-Ch2pEUcw.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-7ANuoZLL.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-DrrqLYAf.js → timeline-definition-O6YCAMPW-07BR8R8G.js} +3 -3
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +1 -0
- package/web/assets/{vennDiagram-MWXL3ELB-B0RkWAim.js → vennDiagram-MWXL3ELB-BQX2ossk.js} +4 -4
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-boUxiQ5F.js → wardleyDiagram-CUQ6CDDI-CGIJBBiK.js} +3 -3
- package/web/assets/{xychartDiagram-N2JHSOCM-BOLm0eNm.js → xychartDiagram-N2JHSOCM-B3HNMuWp.js} +4 -4
- package/web/index.html +17 -10
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-dEkqUZN9.js +0 -36
- package/web/assets/chunk-3SSMPTDK-B4_etUhr.js +0 -321
- package/web/assets/chunk-7W6UQGC5-KyEG0HQg.js +0 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +0 -161
- package/web/assets/classDiagram-KGZ6W3CR-CMne4tG9.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CMne4tG9.js +0 -1
- package/web/assets/cose-bilkent-UX7MHV2Q-D71wNYRJ.js +0 -1
- package/web/assets/dagre-ND4H6XIP-Co5rRx9X.js +0 -4
- package/web/assets/diagram-3NCE3AQN-DBp4O00j.js +0 -43
- package/web/assets/diagram-GF46GFSD-6N_8yS0i.js +0 -24
- package/web/assets/diagram-QXG6HAR7-Di093rFg.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-DZWi4SUN.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +0 -1
- package/web/assets/flowDiagram-H6V6AXG4-CMpY8Ufn.js +0 -162
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +0 -106
- package/web/assets/index-KctnI3Vq.js +0 -635
- package/web/assets/index-_lgn7hs5.css +0 -2
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-BaLMZkUg.js +0 -2
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-DTjVfX4A.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-xY9_k9_Q.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +0 -1
|
@@ -12,6 +12,27 @@ import { markCppInlineNamespaceRange } from './inline-namespaces.js';
|
|
|
12
12
|
import { extractCppTemplateConstraints } from './constraint-extractor.js';
|
|
13
13
|
import { captureCppMemberLookupFacts } from './member-lookup.js';
|
|
14
14
|
import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js';
|
|
15
|
+
import { logger } from '../../../logger.js';
|
|
16
|
+
/**
|
|
17
|
+
* Per-file wall-clock budget for the capture-emit loop (#2432). A worker
|
|
18
|
+
* thread stuck in this loop cannot be terminated safely (terminating a
|
|
19
|
+
* thread mid-N-API call aborts the whole process with Napi::Error), so the
|
|
20
|
+
* loop must bound itself: on breach we return the captures accumulated so
|
|
21
|
+
* far with a warning — degraded coverage for one file, never a crash or a
|
|
22
|
+
* thrown error (a throw here would make the language-group catch drop every
|
|
23
|
+
* remaining file in the batch).
|
|
24
|
+
*
|
|
25
|
+
* `GITNEXUS_CPP_CAPTURE_BUDGET_MS`: unset/invalid/negative → 20000; explicit
|
|
26
|
+
* 0 → expires immediately (deterministic test hook).
|
|
27
|
+
*/
|
|
28
|
+
const CPP_CAPTURE_BUDGET_DEFAULT_MS = 20_000;
|
|
29
|
+
function cppCaptureBudgetMs() {
|
|
30
|
+
const raw = process.env.GITNEXUS_CPP_CAPTURE_BUDGET_MS;
|
|
31
|
+
if (raw === undefined || raw === '')
|
|
32
|
+
return CPP_CAPTURE_BUDGET_DEFAULT_MS;
|
|
33
|
+
const parsed = Number(raw);
|
|
34
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : CPP_CAPTURE_BUDGET_DEFAULT_MS;
|
|
35
|
+
}
|
|
15
36
|
export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
16
37
|
let tree = cachedTree;
|
|
17
38
|
if (tree === undefined) {
|
|
@@ -21,10 +42,27 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
21
42
|
}
|
|
22
43
|
const rawMatches = getCppScopeQuery().matches(tree.rootNode);
|
|
23
44
|
const out = [];
|
|
45
|
+
// #2432: reset the per-file lookup index. The identifier-argument type
|
|
46
|
+
// lookups below used to re-walk the AST per identifier (full-tree DFS in
|
|
47
|
+
// isKnownEnumName, per-scope declaration scans) — O(calls × args × treeSize)
|
|
48
|
+
// per file, 151s on a 194KB file that parses in 46ms. The index makes each
|
|
49
|
+
// lookup O(1) after a single lazily-built pass.
|
|
50
|
+
resetCppFileLookupIndex();
|
|
24
51
|
// Track ranges where typedef-struct/enum was captured as its concrete type
|
|
25
52
|
// so we can suppress the duplicate @declaration.typedef match.
|
|
26
53
|
const concreteTypedefRanges = new Set();
|
|
54
|
+
// #2432: per-file deadline for the loop below (see cppCaptureBudgetMs).
|
|
55
|
+
// Checked every 64 matches — post-index a single iteration is microseconds,
|
|
56
|
+
// so the check granularity costs nothing and bounds the drift past the
|
|
57
|
+
// deadline to well under a second.
|
|
58
|
+
const budgetMs = cppCaptureBudgetMs();
|
|
59
|
+
const deadline = Date.now() + budgetMs;
|
|
60
|
+
let matchIndex = 0;
|
|
27
61
|
for (const m of rawMatches) {
|
|
62
|
+
if ((matchIndex++ & 63) === 0 && Date.now() >= deadline) {
|
|
63
|
+
logger.warn({ filePath, budgetMs, processedMatches: matchIndex - 1, totalMatches: rawMatches.length }, `C++ capture extraction exceeded its ${budgetMs}ms budget for ${filePath}; returning partial captures for this file (#2432).`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
28
66
|
const grouped = {};
|
|
29
67
|
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
|
|
30
68
|
// hands us each matched node as `c.node`, so anchors resolve via a
|
|
@@ -960,6 +998,47 @@ function inferCppBracedInitType(node) {
|
|
|
960
998
|
? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}`
|
|
961
999
|
: `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`;
|
|
962
1000
|
}
|
|
1001
|
+
let fileLookupIndex = {
|
|
1002
|
+
enumNames: null,
|
|
1003
|
+
scopeDecls: new Map(),
|
|
1004
|
+
fnParams: new Map(),
|
|
1005
|
+
};
|
|
1006
|
+
function resetCppFileLookupIndex() {
|
|
1007
|
+
fileLookupIndex = { enumNames: null, scopeDecls: new Map(), fnParams: new Map() };
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* First-declaration-wins map of the scope's `declaration` children that
|
|
1011
|
+
* carry a concrete (non-placeholder) type and a nameable declarator —
|
|
1012
|
+
* exactly the entries the replaced per-identifier scans could match.
|
|
1013
|
+
*/
|
|
1014
|
+
function scopeDeclarationsFor(scope) {
|
|
1015
|
+
const cached = fileLookupIndex.scopeDecls.get(scope.id);
|
|
1016
|
+
if (cached !== undefined)
|
|
1017
|
+
return cached;
|
|
1018
|
+
const decls = new Map();
|
|
1019
|
+
for (let i = 0; i < scope.childCount; i++) {
|
|
1020
|
+
const stmt = scope.child(i);
|
|
1021
|
+
if (stmt === null || stmt.type !== 'declaration')
|
|
1022
|
+
continue;
|
|
1023
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1024
|
+
if (typeNode === null)
|
|
1025
|
+
continue;
|
|
1026
|
+
if (typeNode.type === 'placeholder_type_specifier')
|
|
1027
|
+
continue;
|
|
1028
|
+
const declarator = stmt.childForFieldName('declarator');
|
|
1029
|
+
if (declarator === null)
|
|
1030
|
+
continue;
|
|
1031
|
+
const nameChild = declaredNameNode(declarator);
|
|
1032
|
+
if (nameChild === null)
|
|
1033
|
+
continue;
|
|
1034
|
+
const name = extractDeclaratorLeafName(nameChild);
|
|
1035
|
+
if (name === '' || decls.has(name))
|
|
1036
|
+
continue;
|
|
1037
|
+
decls.set(name, stmt);
|
|
1038
|
+
}
|
|
1039
|
+
fileLookupIndex.scopeDecls.set(scope.id, decls);
|
|
1040
|
+
return decls;
|
|
1041
|
+
}
|
|
963
1042
|
/**
|
|
964
1043
|
* Look up the declared type of a variable by scanning sibling declarations
|
|
965
1044
|
* in the enclosing compound_statement (function body). Handles:
|
|
@@ -991,27 +1070,14 @@ function lookupDeclaredTypeForIdentifier(identNode) {
|
|
|
991
1070
|
const paramType = lookupFunctionParameterType(scope, varName);
|
|
992
1071
|
if (paramType !== '')
|
|
993
1072
|
return paramType;
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
// Skip auto/placeholder types — those need chain-follow, not literal
|
|
1003
|
-
if (typeNode.type === 'placeholder_type_specifier')
|
|
1004
|
-
continue;
|
|
1005
|
-
// Check init_declarator children for the variable name
|
|
1006
|
-
const declarator = stmt.childForFieldName('declarator');
|
|
1007
|
-
if (declarator === null)
|
|
1008
|
-
continue;
|
|
1009
|
-
const nameChild = declaredNameNode(declarator);
|
|
1010
|
-
if (nameChild !== null && extractDeclaratorLeafName(nameChild) === varName) {
|
|
1011
|
-
return normalizeCppTypeText(typeNode.text);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
return '';
|
|
1073
|
+
// Indexed scope-declaration lookup (#2432; was a per-identifier scan).
|
|
1074
|
+
const stmt = scopeDeclarationsFor(scope).get(varName);
|
|
1075
|
+
if (stmt === undefined)
|
|
1076
|
+
return '';
|
|
1077
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1078
|
+
if (typeNode === null)
|
|
1079
|
+
return '';
|
|
1080
|
+
return normalizeCppTypeText(typeNode.text);
|
|
1015
1081
|
}
|
|
1016
1082
|
function lookupDeclaredTypeClassForIdentifier(identNode) {
|
|
1017
1083
|
const varName = identNode.text;
|
|
@@ -1026,28 +1092,20 @@ function lookupDeclaredTypeClassForIdentifier(identNode) {
|
|
|
1026
1092
|
const paramTypeClass = lookupFunctionParameterTypeClass(scope, varName, identNode);
|
|
1027
1093
|
if (paramTypeClass !== undefined)
|
|
1028
1094
|
return paramTypeClass;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const nameChild = declaredNameNode(declarator);
|
|
1042
|
-
if (nameChild === null || extractDeclaratorLeafName(nameChild) !== varName)
|
|
1043
|
-
continue;
|
|
1044
|
-
const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
|
|
1045
|
-
if (isKnownEnumName(identNode, typeClass.base)) {
|
|
1046
|
-
return { ...typeClass, base: `enum:${typeClass.base}` };
|
|
1047
|
-
}
|
|
1048
|
-
return typeClass;
|
|
1095
|
+
// Indexed scope-declaration lookup (#2432; was a per-identifier scan).
|
|
1096
|
+
const stmt = scopeDeclarationsFor(scope).get(varName);
|
|
1097
|
+
if (stmt === undefined)
|
|
1098
|
+
return unknownTypeClass('unknown');
|
|
1099
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1100
|
+
const declarator = stmt.childForFieldName('declarator');
|
|
1101
|
+
const nameChild = declarator !== null ? declaredNameNode(declarator) : null;
|
|
1102
|
+
if (typeNode === null || nameChild === null)
|
|
1103
|
+
return unknownTypeClass('unknown');
|
|
1104
|
+
const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
|
|
1105
|
+
if (isKnownEnumName(identNode, typeClass.base)) {
|
|
1106
|
+
return { ...typeClass, base: `enum:${typeClass.base}` };
|
|
1049
1107
|
}
|
|
1050
|
-
return
|
|
1108
|
+
return typeClass;
|
|
1051
1109
|
}
|
|
1052
1110
|
function lookupFunctionParameterType(scope, varName) {
|
|
1053
1111
|
const param = findEnclosingFunctionParameter(scope, varName);
|
|
@@ -1078,27 +1136,45 @@ function findEnclosingFunctionParameter(scope, varName) {
|
|
|
1078
1136
|
let node = scope.parent;
|
|
1079
1137
|
while (node !== null) {
|
|
1080
1138
|
if (node.type === 'function_definition' || node.type === 'function_declarator') {
|
|
1081
|
-
|
|
1082
|
-
? node
|
|
1083
|
-
: findFirstDescendantOfType(node, 'function_declarator');
|
|
1084
|
-
const params = fnDecl?.childForFieldName('parameters') ?? null;
|
|
1085
|
-
if (params !== null) {
|
|
1086
|
-
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1087
|
-
const param = params.namedChild(i);
|
|
1088
|
-
if (param === null || param.type !== 'parameter_declaration')
|
|
1089
|
-
continue;
|
|
1090
|
-
const declarator = param.childForFieldName('declarator');
|
|
1091
|
-
if (declarator !== null && extractDeclaratorLeafName(declarator) === varName) {
|
|
1092
|
-
return param;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return null;
|
|
1139
|
+
return enclosingFunctionParametersFor(node)?.get(varName) ?? null;
|
|
1097
1140
|
}
|
|
1098
1141
|
node = node.parent;
|
|
1099
1142
|
}
|
|
1100
1143
|
return null;
|
|
1101
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* First-wins map of a function's `parameter_declaration`s by declarator
|
|
1147
|
+
* leaf name (#2432; was a per-identifier scan). `null` when the function
|
|
1148
|
+
* has no parameter list — the caller returns null without walking further
|
|
1149
|
+
* up, preserving the replaced scan's early-return.
|
|
1150
|
+
*/
|
|
1151
|
+
function enclosingFunctionParametersFor(fnNode) {
|
|
1152
|
+
const cached = fileLookupIndex.fnParams.get(fnNode.id);
|
|
1153
|
+
if (cached !== undefined)
|
|
1154
|
+
return cached;
|
|
1155
|
+
const fnDecl = fnNode.type === 'function_declarator'
|
|
1156
|
+
? fnNode
|
|
1157
|
+
: findFirstDescendantOfType(fnNode, 'function_declarator');
|
|
1158
|
+
const params = fnDecl?.childForFieldName('parameters') ?? null;
|
|
1159
|
+
let index = null;
|
|
1160
|
+
if (params !== null) {
|
|
1161
|
+
index = new Map();
|
|
1162
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1163
|
+
const param = params.namedChild(i);
|
|
1164
|
+
if (param === null || param.type !== 'parameter_declaration')
|
|
1165
|
+
continue;
|
|
1166
|
+
const declarator = param.childForFieldName('declarator');
|
|
1167
|
+
if (declarator === null)
|
|
1168
|
+
continue;
|
|
1169
|
+
const name = extractDeclaratorLeafName(declarator);
|
|
1170
|
+
if (name === '' || index.has(name))
|
|
1171
|
+
continue;
|
|
1172
|
+
index.set(name, param);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
fileLookupIndex.fnParams.set(fnNode.id, index);
|
|
1176
|
+
return index;
|
|
1177
|
+
}
|
|
1102
1178
|
function declaredNameNode(declarator) {
|
|
1103
1179
|
if (declarator.type !== 'init_declarator')
|
|
1104
1180
|
return declarator;
|
|
@@ -1126,24 +1202,31 @@ function normalizeCppTypeText(text) {
|
|
|
1126
1202
|
function isKnownEnumName(node, typeName) {
|
|
1127
1203
|
if (typeName === '' || typeName === 'unknown')
|
|
1128
1204
|
return false;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1205
|
+
// One full-tree DFS per FILE (lazy), not per identifier argument — the
|
|
1206
|
+
// per-identifier walk here was the dominant cost of #2432 (87s of a 151s
|
|
1207
|
+
// extraction on a file that parses in 46ms).
|
|
1208
|
+
if (fileLookupIndex.enumNames === null) {
|
|
1209
|
+
let root = node;
|
|
1210
|
+
while (root.parent !== null)
|
|
1211
|
+
root = root.parent;
|
|
1212
|
+
const names = new Set();
|
|
1213
|
+
const stack = [root];
|
|
1214
|
+
while (stack.length > 0) {
|
|
1215
|
+
const cur = stack.pop();
|
|
1216
|
+
if (cur.type === 'enum_specifier') {
|
|
1217
|
+
const name = cur.childForFieldName('name');
|
|
1218
|
+
if (name !== null)
|
|
1219
|
+
names.add(name.text);
|
|
1220
|
+
}
|
|
1221
|
+
for (let i = 0; i < cur.childCount; i++) {
|
|
1222
|
+
const child = cur.child(i);
|
|
1223
|
+
if (child !== null)
|
|
1224
|
+
stack.push(child);
|
|
1225
|
+
}
|
|
1144
1226
|
}
|
|
1227
|
+
fileLookupIndex.enumNames = names;
|
|
1145
1228
|
}
|
|
1146
|
-
return
|
|
1229
|
+
return fileLookupIndex.enumNames.has(typeName);
|
|
1147
1230
|
}
|
|
1148
1231
|
/**
|
|
1149
1232
|
* Detect whether a `namespace_definition` AST node is inline.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { generateId } from '../../lib/utils.js';
|
|
10
|
+
import { toZeroBasedLine } from './utils/line-base.js';
|
|
10
11
|
const HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
11
12
|
const LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
12
13
|
const MD_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
@@ -59,8 +60,8 @@ export const processMarkdown = (graph, files, allPathSet) => {
|
|
|
59
60
|
properties: {
|
|
60
61
|
name: heading,
|
|
61
62
|
filePath: file.path,
|
|
62
|
-
startLine: lineNum,
|
|
63
|
-
endLine,
|
|
63
|
+
startLine: toZeroBasedLine(lineNum),
|
|
64
|
+
endLine: toZeroBasedLine(endLine),
|
|
64
65
|
level,
|
|
65
66
|
description: `h${level}`,
|
|
66
67
|
},
|
|
@@ -3,7 +3,7 @@ import type { SymbolTableWriter } from './model/index.js';
|
|
|
3
3
|
import { type ExportedTypeMap } from './call-processor.js';
|
|
4
4
|
import type { ParsedFile } from '../../_shared/index.js';
|
|
5
5
|
import { WorkerPool } from './workers/worker-pool.js';
|
|
6
|
-
import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
|
|
6
|
+
import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedModuleConstants, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
|
|
7
7
|
import type { ExtractedRouterConstructorPrefix, ExtractedRouterImport, ExtractedRouterInclude, ExtractedRouterModuleAlias } from './route-extractors/fastapi-router-bindings.js';
|
|
8
8
|
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
9
9
|
export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
|
|
@@ -16,6 +16,8 @@ export interface WorkerExtractedData {
|
|
|
16
16
|
routerImports: ExtractedRouterImport[];
|
|
17
17
|
routerConstructorPrefixes: ExtractedRouterConstructorPrefix[];
|
|
18
18
|
routerModuleAliases: ExtractedRouterModuleAlias[];
|
|
19
|
+
/** Per-file Python module constants for cross-file route-path resolution (#2391). */
|
|
20
|
+
moduleConstants: ExtractedModuleConstants[];
|
|
19
21
|
toolDefs: ExtractedToolDef[];
|
|
20
22
|
ormQueries: ExtractedORMQuery[];
|
|
21
23
|
/** Project-wide Spring class/interface views for the #2288 inheritance pass. */
|
|
@@ -24,6 +24,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
24
24
|
const allRouterImports = [];
|
|
25
25
|
const allRouterConstructorPrefixes = [];
|
|
26
26
|
const allRouterModuleAliases = [];
|
|
27
|
+
const allModuleConstants = [];
|
|
27
28
|
const allSpringTypes = [];
|
|
28
29
|
const allToolDefs = [];
|
|
29
30
|
const allORMQueries = [];
|
|
@@ -76,6 +77,8 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
76
77
|
}
|
|
77
78
|
for (const item of result.routerModuleAliases ?? [])
|
|
78
79
|
allRouterModuleAliases.push(item);
|
|
80
|
+
for (const item of result.moduleConstants ?? [])
|
|
81
|
+
allModuleConstants.push(item);
|
|
79
82
|
for (const item of result.springTypes ?? [])
|
|
80
83
|
allSpringTypes.push(item);
|
|
81
84
|
for (const item of result.toolDefs)
|
|
@@ -99,6 +102,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
99
102
|
routerImports: allRouterImports,
|
|
100
103
|
routerConstructorPrefixes: allRouterConstructorPrefixes,
|
|
101
104
|
routerModuleAliases: allRouterModuleAliases,
|
|
105
|
+
moduleConstants: allModuleConstants,
|
|
102
106
|
toolDefs: allToolDefs,
|
|
103
107
|
ormQueries: allORMQueries,
|
|
104
108
|
springTypes: allSpringTypes,
|
|
@@ -28,6 +28,7 @@ import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
|
28
28
|
import { getProvider, providers } from '../languages/index.js';
|
|
29
29
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
30
30
|
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
31
|
+
import { resolveOperands, } from '../route-extractors/python-const-resolver.js';
|
|
31
32
|
import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
|
|
32
33
|
import fs from 'node:fs';
|
|
33
34
|
import path from 'node:path';
|
|
@@ -464,6 +465,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
464
465
|
const allRouterImports = [];
|
|
465
466
|
const allRouterConstructorPrefixes = [];
|
|
466
467
|
const allRouterModuleAliases = [];
|
|
468
|
+
// Per-file Python module constants (#2391); resolved into decorator route paths
|
|
469
|
+
// below, after cross-file aggregation, alongside the include_router prefix pass.
|
|
470
|
+
const allModuleConstants = [];
|
|
467
471
|
const allSpringTypes = [];
|
|
468
472
|
const allToolDefs = [];
|
|
469
473
|
const allORMQueries = [];
|
|
@@ -610,6 +614,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
610
614
|
for (const item of chunkWorkerData.routerModuleAliases)
|
|
611
615
|
allRouterModuleAliases.push(item);
|
|
612
616
|
}
|
|
617
|
+
if (chunkWorkerData.moduleConstants?.length) {
|
|
618
|
+
for (const item of chunkWorkerData.moduleConstants)
|
|
619
|
+
allModuleConstants.push(item);
|
|
620
|
+
}
|
|
613
621
|
if (chunkWorkerData.springTypes?.length) {
|
|
614
622
|
for (const item of chunkWorkerData.springTypes)
|
|
615
623
|
allSpringTypes.push(item);
|
|
@@ -925,6 +933,40 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
925
933
|
}
|
|
926
934
|
// FastAPI router-prefix resolution (cross-file).
|
|
927
935
|
//
|
|
936
|
+
// #2391: resolve non-literal FastAPI decorator route paths (imported/composed
|
|
937
|
+
// string constants) BEFORE the include_router/APIRouter prefix pass below, so a
|
|
938
|
+
// resolved path is then prefix-joined like any literal path. Each such route
|
|
939
|
+
// carries `routePathExpr`/`routePathOperands` and an empty `routePath`; we fold
|
|
940
|
+
// the operands against the repo-wide, file-path-keyed constant map. On failure
|
|
941
|
+
// we DROP the route (KTD5 skip floor) rather than emit a phantom `POST /`.
|
|
942
|
+
if (allDecoratorRoutes.some((dr) => dr.routePathExpr !== undefined)) {
|
|
943
|
+
const repoConstants = new Map();
|
|
944
|
+
for (const { filePath, constants } of allModuleConstants) {
|
|
945
|
+
repoConstants.set(filePath, constants);
|
|
946
|
+
}
|
|
947
|
+
const resolvedRoutes = [];
|
|
948
|
+
let skipped = 0;
|
|
949
|
+
for (const dr of allDecoratorRoutes) {
|
|
950
|
+
if (dr.routePathExpr === undefined) {
|
|
951
|
+
resolvedRoutes.push(dr);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const value = dr.routePathOperands
|
|
955
|
+
? resolveOperands(dr.filePath, dr.routePathOperands, repoConstants)
|
|
956
|
+
: null;
|
|
957
|
+
if (value === null) {
|
|
958
|
+
skipped++;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
resolvedRoutes.push({ ...dr, routePath: value });
|
|
962
|
+
}
|
|
963
|
+
allDecoratorRoutes.length = 0;
|
|
964
|
+
for (const dr of resolvedRoutes)
|
|
965
|
+
allDecoratorRoutes.push(dr);
|
|
966
|
+
if (isDev && skipped > 0) {
|
|
967
|
+
logger.info(` 🧩 Resolved composed route constants; ${skipped} unresolved route(s) skipped`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
928
970
|
// Workers emit two kinds of records per Python file:
|
|
929
971
|
// • `routerIncludes` — every `app.include_router(<routerExpr>, prefix='/x')`
|
|
930
972
|
// site, where `routerExpr` is either `<module>.router` (Shape A) or a
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-agnostic string-constant folding for route-path resolution (#2391).
|
|
3
|
+
*
|
|
4
|
+
* Route decorators/annotations frequently build their path from a constant rather
|
|
5
|
+
* than a string literal — `@router.post(API_V1_WIDGETS_GET)` (Python),
|
|
6
|
+
* `@GetMapping(PathConstants.WIDGETS)` (Spring), and the Kotlin/C# equivalents are
|
|
7
|
+
* the same shape. This module folds such a constant — or an inline
|
|
8
|
+
* `+`-concatenation — to its literal value, following `+` operands and import
|
|
9
|
+
* chains across a repo-wide, file-keyed constant map.
|
|
10
|
+
*
|
|
11
|
+
* The FOLD is language-neutral: it walks {@link Operand} lists and
|
|
12
|
+
* {@link ModuleConstants} that ANY language's extractor can produce, and defers
|
|
13
|
+
* the one language-specific decision — mapping an import specifier to the file it
|
|
14
|
+
* refers to — to a caller-supplied {@link ImportResolver}. A language binding
|
|
15
|
+
* (e.g. `python-const-resolver.ts`) provides that resolver plus a tree →
|
|
16
|
+
* {@link ModuleConstants} extractor and, if wanted, thin pre-bound wrappers.
|
|
17
|
+
*
|
|
18
|
+
* This mirrors how `route-path.ts` (URL normalization) and `spring-shared.ts`
|
|
19
|
+
* (annotation primitives) are shared across the ingestion and group layers and
|
|
20
|
+
* across languages: the reusable core lives in one place; per-language semantics
|
|
21
|
+
* plug in. It deliberately does NOT reuse `ScopeResolver` (which resolves symbol
|
|
22
|
+
* IDENTITIES, not literal string VALUES) or the `--pdg` `REACHING_DEF` layer
|
|
23
|
+
* (intra-procedural, function-local, def→use reachability — not module-level
|
|
24
|
+
* cross-file value folding).
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* One term of a constant's right-hand side. A `+`-concatenation
|
|
28
|
+
* (`A + "/b" + C`) becomes an ordered `Operand[]`; a bare literal is a
|
|
29
|
+
* single-element list.
|
|
30
|
+
*/
|
|
31
|
+
export type Operand = {
|
|
32
|
+
readonly kind: 'literal';
|
|
33
|
+
readonly value: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: 'ref';
|
|
36
|
+
readonly name: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A `from <module> import <name> [as <local>]` (or the language's equivalent)
|
|
40
|
+
* binding. `module` is the import specifier as written (e.g. `.constants`,
|
|
41
|
+
* `..pkg.constants`, `api.constants`) so the {@link ImportResolver} can apply
|
|
42
|
+
* language-specific rules; `originalName` is the exported name in the target
|
|
43
|
+
* module (pre-alias). The map key is the local (in-file) name.
|
|
44
|
+
*/
|
|
45
|
+
export interface ImportBinding {
|
|
46
|
+
readonly module: string;
|
|
47
|
+
readonly originalName: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* String-valued module-level constants of one source file. `literals` are
|
|
51
|
+
* fully-resolved (`X = "/a"`); `exprs` are unresolved operand lists
|
|
52
|
+
* (`X = A + "/b"`); `imports` maps a local name to the module it was imported
|
|
53
|
+
* from. All string keys are the in-file (local) names.
|
|
54
|
+
*/
|
|
55
|
+
export interface ModuleConstants {
|
|
56
|
+
readonly literals: Map<string, string>;
|
|
57
|
+
readonly exprs: Map<string, readonly Operand[]>;
|
|
58
|
+
readonly imports: Map<string, ImportBinding>;
|
|
59
|
+
}
|
|
60
|
+
/** Repo-wide map: unique file key (e.g. `app/constants.py`) → that file's
|
|
61
|
+
* {@link ModuleConstants}. */
|
|
62
|
+
export type RepoConstants = ReadonlyMap<string, ModuleConstants>;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve an import specifier (as written) from `importingFileKey` to the unique
|
|
65
|
+
* repo file key it refers to, or `null` when it cannot be pinned to exactly one
|
|
66
|
+
* file. This is the sole language-specific dependency of the fold: Python uses
|
|
67
|
+
* leading-dot relative imports + `.py`-suffix rules; a JVM binding would use
|
|
68
|
+
* package/classpath rules. Returning `null` on ambiguity keeps the fold honest —
|
|
69
|
+
* an unresolvable or ambiguous import floors to skip, never a wrong path.
|
|
70
|
+
*/
|
|
71
|
+
export type ImportResolver = (importingFileKey: string, moduleSpec: string, repoKeys: ReadonlySet<string>) => string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a single named constant referenced in `fileKey` to its literal string
|
|
74
|
+
* value, folding `+` concatenation and following import chains via
|
|
75
|
+
* `resolveImport`, or `null` when it cannot be fully folded.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveConstant(fileKey: string, name: string, repo: RepoConstants, resolveImport: ImportResolver): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve an inline operand list (an unnamed `+`-expression captured directly at
|
|
80
|
+
* a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
|
|
81
|
+
* against `fileKey`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants, resolveImport: ImportResolver): string | null;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-agnostic string-constant folding for route-path resolution (#2391).
|
|
3
|
+
*
|
|
4
|
+
* Route decorators/annotations frequently build their path from a constant rather
|
|
5
|
+
* than a string literal — `@router.post(API_V1_WIDGETS_GET)` (Python),
|
|
6
|
+
* `@GetMapping(PathConstants.WIDGETS)` (Spring), and the Kotlin/C# equivalents are
|
|
7
|
+
* the same shape. This module folds such a constant — or an inline
|
|
8
|
+
* `+`-concatenation — to its literal value, following `+` operands and import
|
|
9
|
+
* chains across a repo-wide, file-keyed constant map.
|
|
10
|
+
*
|
|
11
|
+
* The FOLD is language-neutral: it walks {@link Operand} lists and
|
|
12
|
+
* {@link ModuleConstants} that ANY language's extractor can produce, and defers
|
|
13
|
+
* the one language-specific decision — mapping an import specifier to the file it
|
|
14
|
+
* refers to — to a caller-supplied {@link ImportResolver}. A language binding
|
|
15
|
+
* (e.g. `python-const-resolver.ts`) provides that resolver plus a tree →
|
|
16
|
+
* {@link ModuleConstants} extractor and, if wanted, thin pre-bound wrappers.
|
|
17
|
+
*
|
|
18
|
+
* This mirrors how `route-path.ts` (URL normalization) and `spring-shared.ts`
|
|
19
|
+
* (annotation primitives) are shared across the ingestion and group layers and
|
|
20
|
+
* across languages: the reusable core lives in one place; per-language semantics
|
|
21
|
+
* plug in. It deliberately does NOT reuse `ScopeResolver` (which resolves symbol
|
|
22
|
+
* IDENTITIES, not literal string VALUES) or the `--pdg` `REACHING_DEF` layer
|
|
23
|
+
* (intra-procedural, function-local, def→use reachability — not module-level
|
|
24
|
+
* cross-file value folding).
|
|
25
|
+
*/
|
|
26
|
+
/** Depth ceiling for the import/constant chase. A heuristic bound, not a proven
|
|
27
|
+
* one; overrun floors to `null` (skip), never a wrong value. */
|
|
28
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
29
|
+
/** Max length of a folded path. Real route paths are well under this; a fold that
|
|
30
|
+
* exceeds it is a pathological self-multiplying concat (`X = A + A; A = B + B; …`)
|
|
31
|
+
* whose true value is genuinely huge — building it risks a `RangeError`/heap OOM,
|
|
32
|
+
* so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
|
|
33
|
+
* NOT output size, which grows multiplicatively; this bounds the output. */
|
|
34
|
+
const MAX_FOLD_LENGTH = 8192;
|
|
35
|
+
/**
|
|
36
|
+
* Fold an operand list to its concatenated literal, or `null` if any operand is
|
|
37
|
+
* unresolvable (an unknown name, a non-string term, a cycle, or a depth overrun).
|
|
38
|
+
*/
|
|
39
|
+
function foldExpr(fileKey, operands, state, depth) {
|
|
40
|
+
if (depth > MAX_RESOLVE_DEPTH)
|
|
41
|
+
return null;
|
|
42
|
+
let out = '';
|
|
43
|
+
for (const op of operands) {
|
|
44
|
+
if (op.kind === 'literal') {
|
|
45
|
+
out += op.value;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const resolved = foldName(fileKey, op.name, state, depth + 1);
|
|
49
|
+
if (resolved === null)
|
|
50
|
+
return null;
|
|
51
|
+
out += resolved;
|
|
52
|
+
}
|
|
53
|
+
if (out.length > MAX_FOLD_LENGTH)
|
|
54
|
+
return null; // pathological self-multiplying concat → drop
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function foldName(fileKey, name, state, depth) {
|
|
59
|
+
if (depth > MAX_RESOLVE_DEPTH)
|
|
60
|
+
return null;
|
|
61
|
+
const guard = `${fileKey}::${name}`;
|
|
62
|
+
// Memoize successful folds. `visited` (below) is the ACTIVE resolution stack
|
|
63
|
+
// for cycle detection — popped on unwind so `A + A` / diamonds fold instead of
|
|
64
|
+
// false-cycling (#2393) — but popping it alone reintroduces recomputation: a
|
|
65
|
+
// wide shared-descendant DAG re-folds each child once per reference, O(fanout^depth),
|
|
66
|
+
// which can exhaust the heap. The never-popped `memo` caps that at O(nodes): a
|
|
67
|
+
// name resolved on one branch is returned directly on the next. Only SUCCESSES
|
|
68
|
+
// are cached — a `null` may be transient (a name that is a cycle on the current
|
|
69
|
+
// branch can resolve on another), so caching it would be unsound.
|
|
70
|
+
const memoized = state.memo.get(guard);
|
|
71
|
+
if (memoized !== undefined)
|
|
72
|
+
return memoized;
|
|
73
|
+
if (state.visited.has(guard))
|
|
74
|
+
return null; // cycle: `name` is on the active stack
|
|
75
|
+
state.visited.add(guard);
|
|
76
|
+
try {
|
|
77
|
+
const result = computeFold(fileKey, name, state, depth);
|
|
78
|
+
if (result !== null)
|
|
79
|
+
state.memo.set(guard, result);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
state.visited.delete(guard);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The literal/expr/import resolution for one name. Cycle guard + memo live in
|
|
87
|
+
* {@link foldName}; this is the pure lookup body. */
|
|
88
|
+
function computeFold(fileKey, name, state, depth) {
|
|
89
|
+
const mc = state.repo.get(fileKey);
|
|
90
|
+
if (!mc)
|
|
91
|
+
return null;
|
|
92
|
+
const literal = mc.literals.get(name);
|
|
93
|
+
if (literal !== undefined)
|
|
94
|
+
return literal;
|
|
95
|
+
const expr = mc.exprs.get(name);
|
|
96
|
+
if (expr !== undefined)
|
|
97
|
+
return foldExpr(fileKey, expr, state, depth + 1);
|
|
98
|
+
const imp = mc.imports.get(name);
|
|
99
|
+
if (imp !== undefined) {
|
|
100
|
+
const targetKey = state.resolveImport(fileKey, imp.module, state.repoKeys);
|
|
101
|
+
if (targetKey === null)
|
|
102
|
+
return null;
|
|
103
|
+
return foldName(targetKey, imp.originalName, state, depth + 1);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function newState(repo, resolveImport) {
|
|
108
|
+
return {
|
|
109
|
+
repo,
|
|
110
|
+
repoKeys: new Set(repo.keys()),
|
|
111
|
+
resolveImport,
|
|
112
|
+
visited: new Set(),
|
|
113
|
+
memo: new Map(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a single named constant referenced in `fileKey` to its literal string
|
|
118
|
+
* value, folding `+` concatenation and following import chains via
|
|
119
|
+
* `resolveImport`, or `null` when it cannot be fully folded.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveConstant(fileKey, name, repo, resolveImport) {
|
|
122
|
+
return foldName(fileKey, name, newState(repo, resolveImport), 0);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve an inline operand list (an unnamed `+`-expression captured directly at
|
|
126
|
+
* a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
|
|
127
|
+
* against `fileKey`.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveOperands(fileKey, operands, repo, resolveImport) {
|
|
130
|
+
return foldExpr(fileKey, operands, newState(repo, resolveImport), 0);
|
|
131
|
+
}
|