@herbertgao/pi-extensions 2026.10.1 → 2026.10.3
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 +14 -17
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
- package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1159 -0
- package/node_modules/pi-lens/README.md +116 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-2FQ62SPM.js +83 -0
- package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
- package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
- package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
- package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
- package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
- package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
- package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
- package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
- package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
- package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
- package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
- package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
- package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
- package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
- package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AFZYMAIV.js +756 -0
- package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
- package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
- package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
- package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
- package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
- package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
- package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
- package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
- package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
- package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
- package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
- package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
- package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
- package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
- package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
- package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
- package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
- package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
- package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
- package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
- package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
- package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
- package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
- package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
- package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
- package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
- package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
- package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
- package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
- package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
- package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
- package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
- package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
- package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
- package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
- package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
- package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
- package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
- package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
- package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
- package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RJFED73B.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
- package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
- package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
- package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
- package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
- package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
- package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
- package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
- package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
- package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
- package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
- package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
- package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
- package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
- package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
- package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
- package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-Z6BB4JLQ.js +478 -0
- package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
- package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
- package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
- package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
- package/node_modules/pi-lens/dist/index.js +39712 -29876
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22688 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -4
- package/node_modules/pi-lens/docs/configuration.md +28 -2
- package/node_modules/pi-lens/docs/custom-rules.md +25 -2
- package/node_modules/pi-lens/docs/environment-variables.md +86 -8
- package/node_modules/pi-lens/docs/features.md +45 -5
- package/node_modules/pi-lens/docs/globalconfig.md +2 -1
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +160 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/schema/pi-lens-config-v1.json +530 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +56 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/docs/word-index.md +6 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +10 -6
- package/node_modules/pi-web-access/ceramic.ts +219 -0
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +846 -588
- package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +34 -2
- package/node_modules/pi-web-access/index.ts +72 -30
- package/node_modules/pi-web-access/openai-search.ts +3 -3
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +6 -9
- package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
- package/node_modules/pi-web-access/query-rewrite.ts +2 -2
- package/node_modules/pi-web-access/storage.ts +49 -0
- package/node_modules/pi-web-access/summary-review.ts +7 -6
- package/node_modules/pi-web-access/utils.ts +21 -6
- package/node_modules/pi-web-access/web-tool-core.ts +1 -0
- package/package.json +19 -19
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
- package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
- package/node_modules/remote-pi/README.md +0 -675
- package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
- package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
- package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
- package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
- package/node_modules/remote-pi/dist/actions/registry.js +0 -58
- package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
- package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
- package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
- package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
- package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
- package/node_modules/remote-pi/dist/config.d.ts +0 -62
- package/node_modules/remote-pi/dist/config.js +0 -115
- package/node_modules/remote-pi/dist/config.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
- package/node_modules/remote-pi/dist/daemon/client.js +0 -131
- package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
- package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
- package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
- package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
- package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
- package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
- package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
- package/node_modules/remote-pi/dist/daemon/id.js +0 -36
- package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
- package/node_modules/remote-pi/dist/daemon/install.js +0 -547
- package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
- package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
- package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
- package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
- package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
- package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
- package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
- package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
- package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
- package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/index.d.ts +0 -213
- package/node_modules/remote-pi/dist/index.js +0 -4996
- package/node_modules/remote-pi/dist/index.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
- package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
- package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
- package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
- package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
- package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
- package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
- package/node_modules/remote-pi/dist/mesh/client.js +0 -125
- package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
- package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
- package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
- package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
- package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
- package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
- package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
- package/node_modules/remote-pi/dist/mesh/types.js +0 -11
- package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
- package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
- package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
- package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
- package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
- package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
- package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
- package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
- package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
- package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
- package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
- package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
- package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
- package/node_modules/remote-pi/dist/protocol/types.js +0 -2
- package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
- package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
- package/node_modules/remote-pi/dist/rooms.js +0 -57
- package/node_modules/remote-pi/dist/rooms.js.map +0 -1
- package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
- package/node_modules/remote-pi/dist/session/address.js +0 -58
- package/node_modules/remote-pi/dist/session/address.js.map +0 -1
- package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
- package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
- package/node_modules/remote-pi/dist/session/bridge.js +0 -130
- package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
- package/node_modules/remote-pi/dist/session/broker.js +0 -507
- package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
- package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
- package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
- package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
- package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
- package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/envelope.js +0 -117
- package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
- package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/global_config.js +0 -73
- package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
- package/node_modules/remote-pi/dist/session/ipc.js +0 -22
- package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
- package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
- package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
- package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
- package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/local_config.js +0 -157
- package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
- package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
- package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
- package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
- package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
- package/node_modules/remote-pi/dist/session/peer.js +0 -451
- package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
- package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
- package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
- package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
- package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
- package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
- package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
- package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
- package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
- package/node_modules/remote-pi/dist/session/tools.js +0 -252
- package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
- package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/wizard.js +0 -20
- package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/settings.d.ts +0 -15
- package/node_modules/remote-pi/dist/settings.js +0 -52
- package/node_modules/remote-pi/dist/settings.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
- package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
- package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
- package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
- package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
- package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
- package/node_modules/remote-pi/dist/ui/footer.js +0 -34
- package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
- package/node_modules/remote-pi/docs/daemon.md +0 -289
- package/node_modules/remote-pi/package.json +0 -92
- package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
- package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
- package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
- package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
- package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
|
@@ -1,4195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree-sitter Structural Search Client for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Inspired by pi-lsp-extension's search-engine.ts and pattern-compiler.ts
|
|
5
|
-
* Provides AST-aware structural search with metavariable capture.
|
|
6
|
-
*
|
|
7
|
-
* Uses web-tree-sitter (WASM) for parsing - no native compilation needed.
|
|
8
|
-
*
|
|
9
|
-
* Pattern syntax:
|
|
10
|
-
* $NAME - Matches any single AST node, captures as NAME
|
|
11
|
-
* $$$NAME - Matches zero or more sibling nodes (variadic)
|
|
12
|
-
*
|
|
13
|
-
* Example:
|
|
14
|
-
* "console.log($MSG)" matches any console.log call, captures argument as MSG
|
|
15
|
-
* "function $NAME($$$PARAMS) { $BODY }" matches function declarations
|
|
16
|
-
*/
|
|
17
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
-
import * as crypto from "node:crypto";
|
|
19
|
-
import * as fs from "node:fs";
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
|
-
import * as path from "node:path";
|
|
22
|
-
import { getDegradationLedgerGeneration, incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
23
|
-
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
24
|
-
import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
|
|
25
|
-
import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
|
|
26
|
-
import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
|
|
27
|
-
import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
|
|
28
|
-
import { getPackageRoot, resolvePackagePath } from "./package-root.js";
|
|
29
|
-
import { resolveWebTreeSitterPackageDir } from "../scripts/lib/web-tree-sitter-dir.mjs";
|
|
30
|
-
import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
|
|
31
|
-
import { escapeRegExp } from "./string-utils.js";
|
|
32
|
-
import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
|
|
33
|
-
import { notifyUserDegradation } from "./user-notify.js";
|
|
34
|
-
const _require = createRequire(import.meta.url);
|
|
35
|
-
import { createTreeCacheCounters, deriveScanTreeCacheCapacity, TreeCache, } from "./tree-sitter-cache.js";
|
|
36
|
-
import { TreeSitterNavigator } from "./tree-sitter-navigator.js";
|
|
37
|
-
import { isProvenSqlAlchemySessionReceiver, isSafePsycopgIdentifierComposition, isSqlAlchemyEntityQueryArgument, isSqlAlchemyStatementArgument, PYTHON_SQLALCHEMY_RECEIVER_NAMES, PYTHON_SQLALCHEMY_STATEMENT_BUILDERS, } from "./python-provenance.js";
|
|
38
|
-
import { TreeSitterQueryLoader, } from "./tree-sitter-query-loader.js";
|
|
39
|
-
// Hard cap on a single structural-search file walk. Bounds a misrooted scan so
|
|
40
|
-
// it can't enumerate an unbounded tree synchronously before result collection
|
|
41
|
-
// short-circuits (#262).
|
|
42
|
-
const TREE_SITTER_MAX_SCAN_FILES = 20_000;
|
|
43
|
-
// Consecutive grammar-load failures after which a batch cache key gives up and
|
|
44
|
-
// caches null like any other deterministic failure. Bounds the retry loop when
|
|
45
|
-
// a grammar simply never loads, while still letting a transient loadLanguage()
|
|
46
|
-
// failure (offline lazy fetch, mid-scan load error) recover on a later scan
|
|
47
|
-
// instead of paying the 3.3x per-rule fallback for the process lifetime (#889).
|
|
48
|
-
const QUERY_BATCH_MAX_LOAD_FAILURES = 3;
|
|
49
|
-
// Keep post-filter tree walks bounded. A malformed or unexpectedly huge tree
|
|
50
|
-
// must not stall the batched query walk; the filter fails open when this cap
|
|
51
|
-
// is reached so unrelated diagnostics and the current match are preserved.
|
|
52
|
-
const NO_NESTED_ANCHOR_VISIT_CAP = 10_000;
|
|
53
|
-
// The execute-style Python DB-API/ORM methods this rule treats as SQL sinks.
|
|
54
|
-
// Hoisted so the post-filter does not rebuild the set per match.
|
|
55
|
-
const PYTHON_SQL_SINK_METHODS = new Set([
|
|
56
|
-
"execute",
|
|
57
|
-
"executemany",
|
|
58
|
-
"query",
|
|
59
|
-
"raw",
|
|
60
|
-
]);
|
|
61
|
-
const TYPESCRIPT_SQL_KNOWN_PACKAGES = new Set([
|
|
62
|
-
"pg",
|
|
63
|
-
"mysql2",
|
|
64
|
-
"better-sqlite3",
|
|
65
|
-
"knex",
|
|
66
|
-
"@prisma/client",
|
|
67
|
-
]);
|
|
68
|
-
const TYPESCRIPT_SQL_IMPORTS = new WeakMap();
|
|
69
|
-
const NOT_PARSED = { parsed: false };
|
|
70
|
-
function createParserCounters() {
|
|
71
|
-
return {
|
|
72
|
-
parserInvocations: 0,
|
|
73
|
-
parserDurationMs: 0,
|
|
74
|
-
parserFailures: 0,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* The errno that blocks writing into `dir`, or undefined when a write can be
|
|
79
|
-
* attempted (#3409). The directory itself need not exist yet — the lazy fetch
|
|
80
|
-
* creates it — so the nearest EXISTING ancestor is what gets probed.
|
|
81
|
-
*/
|
|
82
|
-
function grammarWriteBlockedReason(dir) {
|
|
83
|
-
let probe = dir;
|
|
84
|
-
while (!fs.existsSync(probe)) {
|
|
85
|
-
const parent = path.dirname(probe);
|
|
86
|
-
if (parent === probe)
|
|
87
|
-
return "ENOENT";
|
|
88
|
-
probe = parent;
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
fs.accessSync(probe, fs.constants.W_OK);
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
return err?.code ?? "EACCES";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* `size:mtimeMs` identity for a grammar candidate, or undefined when the path
|
|
100
|
-
* is absent or not a regular file. Doubles as the existence check on the
|
|
101
|
-
* resolve path and as the invalidation key for the verified-preamble memo
|
|
102
|
-
* (#1548) — `size` + `mtimeMs` is the codebase's standard cheap stamp.
|
|
103
|
-
*/
|
|
104
|
-
function grammarFileStamp(filePath) {
|
|
105
|
-
try {
|
|
106
|
-
const stat = fs.statSync(filePath);
|
|
107
|
-
return stat.isFile() ? `${stat.size}:${stat.mtimeMs}` : undefined;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function isTreeSitterWasmAbortError(error) {
|
|
114
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
-
return message.includes("Aborted") || message.includes("abort()");
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Positively-identified RESOLUTION-failure codes for a dynamic `import()`:
|
|
119
|
-
* the specifier couldn't be found/resolved, or a transient fs error hit
|
|
120
|
-
* before `import()` got as far as evaluating the target module. All of
|
|
121
|
-
* these recover once whatever was missing/busy appears — they say nothing
|
|
122
|
-
* durable about the module itself (mirrors the errno allowlist in
|
|
123
|
-
* clients/dispatch/runners/ast-grep-napi.ts's `classifyAstGrepLoadFailure`,
|
|
124
|
-
* kept local here rather than shared because that file is owned by an
|
|
125
|
-
* in-flight PR, #1701, at the time of writing).
|
|
126
|
-
*/
|
|
127
|
-
const RESOLUTION_ERROR_CODES = new Set([
|
|
128
|
-
"ERR_MODULE_NOT_FOUND",
|
|
129
|
-
"MODULE_NOT_FOUND",
|
|
130
|
-
"ERR_INVALID_MODULE_SPECIFIER",
|
|
131
|
-
"ERR_UNSUPPORTED_DIR_IMPORT",
|
|
132
|
-
"ENOENT",
|
|
133
|
-
"EMFILE",
|
|
134
|
-
"EBUSY",
|
|
135
|
-
"EAGAIN",
|
|
136
|
-
"EPERM",
|
|
137
|
-
"ETXTBSY",
|
|
138
|
-
]);
|
|
139
|
-
/** Walk an error's `.cause` chain, collecting every `code` seen along the
|
|
140
|
-
* way — a resolution failure's `code` can be wrapped in a cause rather than
|
|
141
|
-
* sitting on the top-level thrown Error. */
|
|
142
|
-
function collectErrorCodes(err) {
|
|
143
|
-
const codes = [];
|
|
144
|
-
let current = err;
|
|
145
|
-
const seen = new Set();
|
|
146
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
147
|
-
seen.add(current);
|
|
148
|
-
const code = current.code;
|
|
149
|
-
if (typeof code === "string")
|
|
150
|
-
codes.push(code);
|
|
151
|
-
current = current.cause;
|
|
152
|
-
}
|
|
153
|
-
return codes;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Classify a `loadWebTreeSitter()` rejection (#1592). RESOLUTION failures
|
|
157
|
-
* (a positively-identified code above) are recoverable: the module gets a
|
|
158
|
-
* fresh `import()` attempt on the very next `init()` call, same as before
|
|
159
|
-
* this fix, because whatever blocked resolution can plausibly clear on its
|
|
160
|
-
* own. Everything else is treated as EVALUATION-shaped and latches — an
|
|
161
|
-
* unrecognized error is far more likely to be the target module's own
|
|
162
|
-
* top-level code throwing (which Node's ESM loader then memoizes as a
|
|
163
|
-
* permanently-rejected module record for that URL) than an unclassified
|
|
164
|
-
* resolution hiccup, so "unknown" defaults to the case a retry cannot fix.
|
|
165
|
-
*/
|
|
166
|
-
function classifyWebTreeSitterLoadFailure(err) {
|
|
167
|
-
const codes = collectErrorCodes(err);
|
|
168
|
-
return codes.some((code) => RESOLUTION_ERROR_CODES.has(code))
|
|
169
|
-
? "resolution"
|
|
170
|
-
: "evaluation";
|
|
171
|
-
}
|
|
172
|
-
// --- Parser Manager ---
|
|
173
|
-
export class TreeSitterClient {
|
|
174
|
-
initialized = false;
|
|
175
|
-
initPromise = null;
|
|
176
|
-
/**
|
|
177
|
-
* Set when `loadWebTreeSitter()` itself rejects with an EVALUATION-shaped
|
|
178
|
-
* error (#1592, review round 2 F1/F2) — `classifyWebTreeSitterLoadFailure`
|
|
179
|
-
* above draws that line. That call is a dynamic `import()` of a fixed
|
|
180
|
-
* resolved URL, and Node's ESM loader permanently memoizes a module
|
|
181
|
-
* record that threw during evaluation — a later `import()` of the SAME
|
|
182
|
-
* URL from a later `init()` call would just replay the cached rejection,
|
|
183
|
-
* not re-attempt the load. Without this latch, every `withTreeSitterRoot()`
|
|
184
|
-
* call (one per file parse) would re-invoke `init()`, see `initPromise`
|
|
185
|
-
* cleared by the previous attempt's `finally`, and dynamically re-import —
|
|
186
|
-
* a dead retry on the hot path. A RESOLUTION-shaped rejection does NOT
|
|
187
|
-
* set this: it leaves `initPromise` cleared as before, so the next
|
|
188
|
-
* `init()` call retries for real, because that class of failure can
|
|
189
|
-
* plausibly clear before the next call arrives.
|
|
190
|
-
*
|
|
191
|
-
* SESSION-scoped, not process-lifetime (round 2 F2, the #1567/#1575
|
|
192
|
-
* `sgSessionHold` precedent): re-armed by `resetLoadStateForSession()`,
|
|
193
|
-
* wired into `resetDispatchBaselines()` via `tree-sitter-shared.ts`'s
|
|
194
|
-
* `resetTreeSitterClientLoadState()`. Re-arming does not guarantee the
|
|
195
|
-
* next attempt succeeds — if the process itself didn't restart between
|
|
196
|
-
* sessions, Node's module cache is unchanged and the replay will just
|
|
197
|
-
* fail fast again — but it gives a fresh degradation record for the new
|
|
198
|
-
* session (the ledger's own `onceKeys` are cleared at session_start too)
|
|
199
|
-
* instead of silently reusing a stale verdict forever, and it does let a
|
|
200
|
-
* genuinely fixed install (process WAS restarted) recover.
|
|
201
|
-
*
|
|
202
|
-
* Distinct from `wasmAborted`, which stays process-lifetime and is
|
|
203
|
-
* deliberately NOT included in the session reset: that flag means the
|
|
204
|
-
* Emscripten WASM heap itself aborted mid-use — a runtime that DID load
|
|
205
|
-
* and then corrupted its own memory. Reusing that heap after a "session"
|
|
206
|
-
* boundary that isn't an actual process restart is not a retry, it's
|
|
207
|
-
* operating on data already documented (tree-sitter-shared.ts) as
|
|
208
|
-
* requiring a real restart to recover. `webTreeSitterLoadFailed` only
|
|
209
|
-
* ever covers a runtime that never loaded in the first place, so nothing
|
|
210
|
-
* corrupted survives a re-arm.
|
|
211
|
-
*/
|
|
212
|
-
webTreeSitterLoadFailed = false;
|
|
213
|
-
languages = new Map();
|
|
214
|
-
parsers = new Map();
|
|
215
|
-
treeCache;
|
|
216
|
-
navigator = new TreeSitterNavigator();
|
|
217
|
-
grammarsDir;
|
|
218
|
-
/** In-flight lazy grammar fetches, keyed by wasm filename. Evicted on
|
|
219
|
-
* settle (#1536) — a rejected/false attempt must not be remembered as the
|
|
220
|
-
* permanent answer for the session; only concurrent demands during the
|
|
221
|
-
* SAME probe share it. */
|
|
222
|
-
grammarEnsurePromises = new Map();
|
|
223
|
-
/** Epoch ms before which a failed grammar download is not retried
|
|
224
|
-
* (#1536). A transient download failure — offline, DNS hiccup, CDN blip —
|
|
225
|
-
* says nothing durable about the grammar, so it gets a bounded cooldown
|
|
226
|
-
* (the `transientRetryDelayMs` shape from availability-policy.ts) instead
|
|
227
|
-
* of latching for the process lifetime. */
|
|
228
|
-
grammarRetryAtMs = new Map();
|
|
229
|
-
/**
|
|
230
|
-
* Grammar path → the `size:mtimeMs` stamp at which its wasm preamble was
|
|
231
|
-
* verified (#1548). A POSITIVE-only memo, so the steady-state resolve costs
|
|
232
|
-
* one `stat` (which the old `existsSync` already paid) and re-reads the
|
|
233
|
-
* header only when the stamp moves. Stamped rather than a bare `Set`
|
|
234
|
-
* because a memo that never expires is defect-shape 6: the file it vouches
|
|
235
|
-
* for can be replaced under it. Failures are deliberately NOT memoized — a
|
|
236
|
-
* poisoned path must be re-examined on the next resolve so a successful
|
|
237
|
-
* re-download over it is picked up immediately.
|
|
238
|
-
*/
|
|
239
|
-
verifiedGrammarPaths = new Map();
|
|
240
|
-
/**
|
|
241
|
-
* Grammar path → the `size:mtimeMs` stamp at which `Language.load` failed
|
|
242
|
-
* on it despite passing the wasm-preamble check (#1564) — e.g. a truncated
|
|
243
|
-
* download whose first four bytes are a genuine `\0asm` preamble but whose
|
|
244
|
-
* body decodes short ("Code section extends past end of the module"). The
|
|
245
|
-
* preamble check alone can't see this; only a real decode attempt can.
|
|
246
|
-
* `resolveGrammarFile` treats a path recorded here (at the SAME stamp) as
|
|
247
|
-
* absent, so the next demand re-fetches instead of reusing the same broken
|
|
248
|
-
* file forever. Stamped so a fresh download (new mtime) is re-examined
|
|
249
|
-
* rather than permanently distrusted — same shape as `verifiedGrammarPaths`.
|
|
250
|
-
*/
|
|
251
|
-
decodeFailedGrammarPaths = new Map();
|
|
252
|
-
/**
|
|
253
|
-
* Grammar path → the `size:mtimeMs` stamp at which its bytes were verified
|
|
254
|
-
* to match the CURRENTLY PINNED sha256 manifest (#1760). Positive-only,
|
|
255
|
-
* same discipline as `verifiedGrammarPaths`: the hash is computed once per
|
|
256
|
-
* stamp (not per parse), and a stamp change — a fresh download — forces a
|
|
257
|
-
* re-check rather than trusting a memo made for different bytes.
|
|
258
|
-
*/
|
|
259
|
-
verifiedGrammarVersionAt = new Map();
|
|
260
|
-
/**
|
|
261
|
-
* Grammar path → the `size:mtimeMs` stamp at which it was found to no
|
|
262
|
-
* longer match the pinned manifest hash — a version bump
|
|
263
|
-
* (`TREE_SITTER_WASMS_VERSION`, or a `SOURCE_OVERRIDES` entry), or on-disk
|
|
264
|
-
* corruption (#1760). `resolveGrammarFile` treats a path recorded here (at
|
|
265
|
-
* the SAME stamp) as absent, exactly like `decodeFailedGrammarPaths`, so
|
|
266
|
-
* the next demand re-fetches instead of serving the stale/corrupt file for
|
|
267
|
-
* the rest of the process's life. A fresh download (new stamp) is
|
|
268
|
-
* re-examined rather than permanently distrusted.
|
|
269
|
-
*/
|
|
270
|
-
staleGrammarVersionAt = new Map();
|
|
271
|
-
/**
|
|
272
|
-
* Paths already reported as version-stale THIS SESSION, to log/record them
|
|
273
|
-
* once each (#1801 review F1). Deliberately separate from
|
|
274
|
-
* `staleGrammarVersionAt`: that map is a pure hash-verdict memo whose whole
|
|
275
|
-
* point is to persist for the process's life (re-hashing an unchanged file
|
|
276
|
-
* is exactly what it exists to avoid), so it must never double as a report
|
|
277
|
-
* gate — a gate riding on process-lifetime state can't re-arm at a session
|
|
278
|
-
* boundary. Cleared in `refreshGrammarSessionLatches`, mirroring
|
|
279
|
-
* `poisonedGrammarPaths`.
|
|
280
|
-
*/
|
|
281
|
-
staleReportedGrammarPaths = new Set();
|
|
282
|
-
/** Paths already reported as non-wasm, to log/record them once each. */
|
|
283
|
-
poisonedGrammarPaths = new Set();
|
|
284
|
-
/** Consecutive download failures per grammar, for the exponential
|
|
285
|
-
* cooldown; reset on success. */
|
|
286
|
-
grammarFailureAttempts = new Map();
|
|
287
|
-
/**
|
|
288
|
-
* Retry delay (ms) last SHOWN to the user for this grammar, or `-1` for a
|
|
289
|
-
* durable (non-retryable) failure already announced. The cooldown lets
|
|
290
|
-
* the ensure loop retry silently in the background — only the delay's
|
|
291
|
-
* FIRST appearance, and any later escalation the user was never told
|
|
292
|
-
* about, needs to interrupt them (#1536 review F6): a fresh 30s cooldown
|
|
293
|
-
* re-notifies the same as an escalated 300s one once it's a genuinely new
|
|
294
|
-
* number, but two consecutive 30s cooldowns (identical, nothing new to
|
|
295
|
-
* say) do not. Absence means "not yet notified this streak."
|
|
296
|
-
*
|
|
297
|
-
* Session-scoped (#1536 review F5): cleared whenever the degradation
|
|
298
|
-
* ledger's own generation moves past `grammarNotificationsLedgerGen`,
|
|
299
|
-
* mirroring `trustBlockedGrammarNotifications` below — a lazy
|
|
300
|
-
* compare-at-use-time against a monotonic counter, not a listener. Tied to
|
|
301
|
-
* the LEDGER's generation (bumped by `resetDegradationLedger`, which
|
|
302
|
-
* `handleSessionStart` calls first thing) rather than trust, since this
|
|
303
|
-
* is a session boundary, not a trust transition.
|
|
304
|
-
*/
|
|
305
|
-
grammarLastNotifiedDelayMs = new Map();
|
|
306
|
-
grammarNotificationsLedgerGen = getDegradationLedgerGeneration();
|
|
307
|
-
trustBlockedGrammarNotifications = new Set();
|
|
308
|
-
trustNotificationsGeneration = getProjectTrustGeneration();
|
|
309
|
-
// biome-ignore lint/suspicious/noExplicitAny: Optional dependency loaded dynamically
|
|
310
|
-
ParserClass = null;
|
|
311
|
-
// biome-ignore lint/suspicious/noExplicitAny: Language loader from module
|
|
312
|
-
LanguageLoader = null;
|
|
313
|
-
// Declared BEFORE the two caches below: a static read by an instance field
|
|
314
|
-
// initializer must already be initialized (TS2729).
|
|
315
|
-
static QUERY_CACHE_MAX_ENTRIES = 256;
|
|
316
|
-
static QUERY_BATCH_CACHE_MAX_ENTRIES = 256;
|
|
317
|
-
// biome-ignore lint/suspicious/noExplicitAny: Compiled query cache by language+pattern hash
|
|
318
|
-
// BoundedFifoMap, not BoundedLruCache: recency here is refreshed by this
|
|
319
|
-
// class's own explicit delete+set on both the read and the write path (see
|
|
320
|
-
// cacheQuery and the two lookup sites) — exactly the raw-`Map` discipline
|
|
321
|
-
// the FIFO map documents. A get() that promoted on its own would change
|
|
322
|
-
// which entry eviction targets (#2442 review F5/F7).
|
|
323
|
-
queryCache = new BoundedFifoMap(TreeSitterClient.QUERY_CACHE_MAX_ENTRIES);
|
|
324
|
-
/** Combined multi-rule queries by language + rule-set identity (null = don't retry). */
|
|
325
|
-
queryBatchCache = new BoundedFifoMap(TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES);
|
|
326
|
-
queryCacheCap() {
|
|
327
|
-
const value = Number.parseInt(process.env.PI_LENS_TREE_SITTER_QUERY_CACHE_CAP ?? "", 10);
|
|
328
|
-
return Number.isSafeInteger(value) && value > 0
|
|
329
|
-
? value
|
|
330
|
-
: TreeSitterClient.QUERY_CACHE_MAX_ENTRIES;
|
|
331
|
-
}
|
|
332
|
-
queryBatchCacheCap() {
|
|
333
|
-
const value = Number.parseInt(process.env.PI_LENS_TREE_SITTER_QUERY_BATCH_CACHE_CAP ?? "", 10);
|
|
334
|
-
return Number.isSafeInteger(value) && value > 0
|
|
335
|
-
? value
|
|
336
|
-
: TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES;
|
|
337
|
-
}
|
|
338
|
-
// biome-ignore lint/suspicious/noExplicitAny: compiled query objects
|
|
339
|
-
cacheQuery(key, value) {
|
|
340
|
-
this.queryCache.delete(key); // write-refresh: this write is the newest
|
|
341
|
-
// The cap is env-readable, so re-apply it before every write — a raised
|
|
342
|
-
// or lowered ceiling has to take effect on the next insert, not on the
|
|
343
|
-
// next restart. Both calls hand back the dropped [key, value] pairs so
|
|
344
|
-
// the evicted compiled query's native handle is still freed (#2442
|
|
345
|
-
// review F5/F7 — this is why set() returns the VALUE, not just the key).
|
|
346
|
-
const evicted = this.queryCache.setMaxEntries(this.queryCacheCap());
|
|
347
|
-
evicted.push(...this.queryCache.set(key, value));
|
|
348
|
-
for (const [, dropped] of evicted)
|
|
349
|
-
dropped?.query?.delete?.();
|
|
350
|
-
}
|
|
351
|
-
cacheQueryBatch(key, value) {
|
|
352
|
-
this.queryBatchCache.delete(key);
|
|
353
|
-
const evicted = this.queryBatchCache.setMaxEntries(this.queryBatchCacheCap());
|
|
354
|
-
evicted.push(...this.queryBatchCache.set(key, value));
|
|
355
|
-
for (const [, dropped] of evicted)
|
|
356
|
-
dropped?.query?.delete?.();
|
|
357
|
-
}
|
|
358
|
-
/** Consecutive grammar-load failures per batch key — bounds load retries (#889). */
|
|
359
|
-
queryBatchLoadFailures = new Map();
|
|
360
|
-
queryLoader = new TreeSitterQueryLoader();
|
|
361
|
-
verbose;
|
|
362
|
-
parserCounters = createParserCounters();
|
|
363
|
-
parseCacheMeasurement = new AsyncLocalStorage();
|
|
364
|
-
activeMeasurements = 0;
|
|
365
|
-
onWasmAbort;
|
|
366
|
-
wasmAborted = false;
|
|
367
|
-
grammarDirResolutionDeps;
|
|
368
|
-
constructor(verbose = false, onWasmAbort, grammarDirResolutionDeps) {
|
|
369
|
-
this.grammarDirResolutionDeps = grammarDirResolutionDeps ?? {
|
|
370
|
-
resolveAsset: (asset) => this.resolveWebTreeSitterAsset(asset),
|
|
371
|
-
resolvePackage: (specifier) => _require.resolve(specifier),
|
|
372
|
-
packageRoot: () => getPackageRoot(import.meta.url),
|
|
373
|
-
cwd: () => process.cwd(),
|
|
374
|
-
};
|
|
375
|
-
this.grammarsDir = this.findGrammarsDir();
|
|
376
|
-
this.verbose = verbose;
|
|
377
|
-
this.onWasmAbort = onWasmAbort;
|
|
378
|
-
this.treeCache = new TreeCache(50, verbose, 4096, (key, amount) => {
|
|
379
|
-
const measurement = this.parseCacheMeasurement.getStore();
|
|
380
|
-
if (measurement)
|
|
381
|
-
measurement[key] += amount;
|
|
382
|
-
}, (error) => this.reportWasmAbort(error));
|
|
383
|
-
}
|
|
384
|
-
recordParserCounter(key, amount = 1) {
|
|
385
|
-
this.parserCounters[key] += amount;
|
|
386
|
-
const measurement = this.parseCacheMeasurement.getStore();
|
|
387
|
-
if (measurement)
|
|
388
|
-
measurement[key] += amount;
|
|
389
|
-
}
|
|
390
|
-
reportWasmAbort(error) {
|
|
391
|
-
if (!isTreeSitterWasmAbortError(error))
|
|
392
|
-
return false;
|
|
393
|
-
if (!this.wasmAborted) {
|
|
394
|
-
this.wasmAborted = true;
|
|
395
|
-
recordDegradation({
|
|
396
|
-
kind: "wasm-abort",
|
|
397
|
-
subject: "web-tree-sitter",
|
|
398
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
399
|
-
});
|
|
400
|
-
this.onWasmAbort?.();
|
|
401
|
-
}
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* O(1) runtime footprint counters (#1123 item 2 memory attribution): every
|
|
406
|
-
* field is a `Map.size` read, never an iteration over the maps' contents.
|
|
407
|
-
* `wasmMemoryBytes` is deliberately NOT included here — web-tree-sitter
|
|
408
|
-
* 0.25.10's Emscripten `Module` (which owns the WASM linear memory /
|
|
409
|
-
* `HEAPU8.buffer`) is a private closure variable in the package's
|
|
410
|
-
* `bindings.ts` and is not exposed through any public export (`Parser`,
|
|
411
|
-
* `Language`, `Query`, ...); reaching it would require either reflecting
|
|
412
|
-
* into the package's internal module state (brittle across web-tree-sitter
|
|
413
|
-
* versions/bundling) or overriding Emscripten's `wasmMemory` module option
|
|
414
|
-
* at `Parser.init()` time with a hand-constructed `WebAssembly.Memory`
|
|
415
|
-
* matching the library's own default page-count math (risks a memory-import
|
|
416
|
-
* mismatch that would break ALL structural analysis, for an
|
|
417
|
-
* observability-only feature). `process.memoryUsage().arrayBuffers` is the
|
|
418
|
-
* process-wide proxy used instead (WASM linear memory backs an ArrayBuffer,
|
|
419
|
-
* so it is included there) — see clients/memory-sampler.ts.
|
|
420
|
-
*/
|
|
421
|
-
getRuntimeStats() {
|
|
422
|
-
return {
|
|
423
|
-
languagesLoaded: this.languages.size,
|
|
424
|
-
parsersLoaded: this.parsers.size,
|
|
425
|
-
queryCacheSize: this.queryCache.size,
|
|
426
|
-
queryBatchCacheSize: this.queryBatchCache.size,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
getParseCacheStats() {
|
|
430
|
-
return {
|
|
431
|
-
...this.treeCache.getStats(),
|
|
432
|
-
...this.parserCounters,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Grow the tree cache to span a full-project scan's working set (#1715).
|
|
437
|
-
* The interactive default (50 entries) can't hold a mid/large project's
|
|
438
|
-
* file count, so a second scan re-parses everything the first scan's LRU
|
|
439
|
-
* evicted — live dogfood evidence showed every miss on a second 110-file
|
|
440
|
-
* scan was a `capacityMisses` one. Bounded by
|
|
441
|
-
* `TREE_CACHE_SCAN_CAPACITY_CEILING` (see `deriveScanTreeCacheCapacity`'s
|
|
442
|
-
* heap-cost note) and monotonic — a smaller scan never shrinks a capacity
|
|
443
|
-
* an earlier, larger one already grew.
|
|
444
|
-
*/
|
|
445
|
-
ensureTreeCacheCapacity(fileCount) {
|
|
446
|
-
this.treeCache.setMaxSize(deriveScanTreeCacheCapacity(fileCount, this.treeCache.getMaxSize()));
|
|
447
|
-
}
|
|
448
|
-
async withParseCacheMeasurement(work, onComplete) {
|
|
449
|
-
const measurement = {
|
|
450
|
-
...createTreeCacheCounters(),
|
|
451
|
-
...createParserCounters(),
|
|
452
|
-
};
|
|
453
|
-
this.activeMeasurements++;
|
|
454
|
-
try {
|
|
455
|
-
return await this.parseCacheMeasurement.run(measurement, async () => {
|
|
456
|
-
try {
|
|
457
|
-
return await work();
|
|
458
|
-
}
|
|
459
|
-
finally {
|
|
460
|
-
const cacheStats = this.treeCache.getStats();
|
|
461
|
-
try {
|
|
462
|
-
onComplete({
|
|
463
|
-
...cacheStats,
|
|
464
|
-
...measurement,
|
|
465
|
-
misses: measurement.lookups - measurement.hits,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
// Telemetry must not fail the measured work.
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
finally {
|
|
475
|
-
// On Node 22 an active AsyncLocalStorage keeps an async_hooks init hook
|
|
476
|
-
// enabled for every promise in the process, so release it once the last
|
|
477
|
-
// overlapping measurement is done rather than taxing the whole session.
|
|
478
|
-
if (--this.activeMeasurements === 0)
|
|
479
|
-
this.parseCacheMeasurement.disable();
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
/** Debug logging helper */
|
|
483
|
-
dbg(msg) {
|
|
484
|
-
if (this.verbose) {
|
|
485
|
-
// #1333: verbose gating is preserved, but the SINK is tree-sitter.log —
|
|
486
|
-
// a raw write would corrupt pi's frame the moment verbose is enabled.
|
|
487
|
-
logTreeSitterDiagnostic({
|
|
488
|
-
subsystem: "tree-sitter-client",
|
|
489
|
-
level: "debug",
|
|
490
|
-
message: msg,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* web-tree-sitter's installed package directory, through the one shared
|
|
496
|
-
* ladder in `scripts/lib/web-tree-sitter-dir.mjs` (#3409). Routed through
|
|
497
|
-
* `grammarDirResolutionDeps` so the resolver context — the thing that
|
|
498
|
-
* differs on a compiled host — stays the injected seam (#2112/#2136).
|
|
499
|
-
*/
|
|
500
|
-
webTreeSitterPackageDir() {
|
|
501
|
-
return resolveWebTreeSitterPackageDir({
|
|
502
|
-
resolve: (specifier) => this.grammarDirResolutionDeps.resolvePackage(specifier),
|
|
503
|
-
packageRoot: () => this.grammarDirResolutionDeps.packageRoot(),
|
|
504
|
-
cwd: () => this.grammarDirResolutionDeps.cwd(),
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Resolve a web-tree-sitter asset path:
|
|
509
|
-
* 1. the asset's own subpath, authoritative when the package `exports` it
|
|
510
|
-
* (`tree-sitter.wasm` is exported; `grammars` is NOT in 0.25.10, so this
|
|
511
|
-
* rung is dead for the grammars dir — #3409's second symptom);
|
|
512
|
-
* 2. the asset inside the package directory the shared ladder resolves. That
|
|
513
|
-
* ladder's own last two rungs are the package-root walk (#20, for pi's
|
|
514
|
-
* temp-dir compile) and the cwd fallback this method used to spell out.
|
|
515
|
-
*/
|
|
516
|
-
resolveWebTreeSitterAsset(asset) {
|
|
517
|
-
try {
|
|
518
|
-
const resolved = this.grammarDirResolutionDeps.resolvePackage(`web-tree-sitter/${asset}`);
|
|
519
|
-
if (fs.existsSync(resolved))
|
|
520
|
-
return resolved;
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
/* fall through */
|
|
524
|
-
}
|
|
525
|
-
const pkgDir = this.webTreeSitterPackageDir();
|
|
526
|
-
if (pkgDir) {
|
|
527
|
-
const candidate = path.join(pkgDir, asset);
|
|
528
|
-
if (fs.existsSync(candidate))
|
|
529
|
-
return candidate;
|
|
530
|
-
}
|
|
531
|
-
return undefined;
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* The `grammars/` dir bundled inside the pi-lens package (the core grammars
|
|
535
|
-
* shipped in the tarball, so common languages parse offline on every package
|
|
536
|
-
* manager). Resolved from the package root; cached. Absent in a source
|
|
537
|
-
* checkout where `prepare` hasn't populated it.
|
|
538
|
-
*/
|
|
539
|
-
_bundledGrammarsDir;
|
|
540
|
-
bundledGrammarsDir() {
|
|
541
|
-
// Cache only a positive hit; keep re-checking until it exists (prepare may
|
|
542
|
-
// not have populated it yet at first probe).
|
|
543
|
-
if (this._bundledGrammarsDir)
|
|
544
|
-
return this._bundledGrammarsDir;
|
|
545
|
-
try {
|
|
546
|
-
const dir = resolvePackagePath(import.meta.url, "grammars");
|
|
547
|
-
if (fs.existsSync(dir))
|
|
548
|
-
this._bundledGrammarsDir = dir;
|
|
549
|
-
return this._bundledGrammarsDir;
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
return undefined;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
_vendoredGrammarsDir;
|
|
556
|
-
/**
|
|
557
|
-
* The committed `vendor/grammars` dir, if it exists. Cached only on a hit,
|
|
558
|
-
* mirroring `bundledGrammarsDir`.
|
|
559
|
-
*/
|
|
560
|
-
vendoredGrammarsDir() {
|
|
561
|
-
if (this._vendoredGrammarsDir)
|
|
562
|
-
return this._vendoredGrammarsDir;
|
|
563
|
-
try {
|
|
564
|
-
const dir = vendoredGrammarsDir();
|
|
565
|
-
if (fs.existsSync(dir))
|
|
566
|
-
this._vendoredGrammarsDir = dir;
|
|
567
|
-
return this._vendoredGrammarsDir;
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
return undefined;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* All directories that may hold grammar wasms, in precedence order: the
|
|
575
|
-
* committed vendor dir, the bundled core dir, the resolved
|
|
576
|
-
* `this.grammarsDir`, and the web-tree-sitter grammars dir (the lazy-fetch
|
|
577
|
-
* write target). Deduped.
|
|
578
|
-
*
|
|
579
|
-
* `vendor/grammars` comes first because it is the ONLY source for a grammar
|
|
580
|
-
* we build ourselves (`VENDORED_GRAMMARS`) — nothing downloads into the
|
|
581
|
-
* later dirs for it, so a miss here is a miss everywhere.
|
|
582
|
-
*/
|
|
583
|
-
grammarSourceDirs() {
|
|
584
|
-
const dirs = [];
|
|
585
|
-
const push = (d) => {
|
|
586
|
-
if (d && !dirs.includes(d))
|
|
587
|
-
dirs.push(d);
|
|
588
|
-
};
|
|
589
|
-
push(this.vendoredGrammarsDir());
|
|
590
|
-
push(this.bundledGrammarsDir());
|
|
591
|
-
push(this.grammarsDir || undefined);
|
|
592
|
-
push(this.resolveWebTreeSitterAsset("grammars"));
|
|
593
|
-
return dirs;
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Absolute path to `grammarFile` across all source dirs, else undefined.
|
|
597
|
-
*
|
|
598
|
-
* Existence is not enough (#1548): a file whose first four bytes aren't the
|
|
599
|
-
* wasm preamble is not a grammar, and returning it would report the language
|
|
600
|
-
* as available while every `Language.load` fails — the permanent-poisoning
|
|
601
|
-
* shape this issue is about. #1548 stops NEW poisoned files from being
|
|
602
|
-
* written, but a file poisoned before that shipped is already on disk, so
|
|
603
|
-
* the resolve path has to reject it too: a rejected candidate is treated as
|
|
604
|
-
* absent, which lets `ensureGrammar` re-download over it.
|
|
605
|
-
*/
|
|
606
|
-
resolveGrammarFile(grammarFile) {
|
|
607
|
-
for (const dir of this.grammarSourceDirs()) {
|
|
608
|
-
const candidate = path.join(dir, grammarFile);
|
|
609
|
-
const stamp = grammarFileStamp(candidate);
|
|
610
|
-
if (!stamp)
|
|
611
|
-
continue;
|
|
612
|
-
// #1564: a file that passed the preamble check but failed
|
|
613
|
-
// `Language.load` at this exact stamp is known-bad — treat it as
|
|
614
|
-
// absent so `ensureGrammar` re-fetches over it, same as the
|
|
615
|
-
// non-wasm-magic case below. A stamp mismatch (the file changed —
|
|
616
|
-
// re-downloaded) falls through to re-examine it fresh.
|
|
617
|
-
if (this.decodeFailedGrammarPaths.get(candidate) === stamp)
|
|
618
|
-
continue;
|
|
619
|
-
if (this.verifiedGrammarPaths.get(candidate) === stamp) {
|
|
620
|
-
if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
|
|
621
|
-
return candidate;
|
|
622
|
-
}
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (fileHasWasmMagic(candidate)) {
|
|
626
|
-
this.verifiedGrammarPaths.set(candidate, stamp);
|
|
627
|
-
if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
|
|
628
|
-
return candidate;
|
|
629
|
-
}
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
this.verifiedGrammarPaths.delete(candidate);
|
|
633
|
-
this.reportPoisonedGrammarFile(candidate, grammarFile);
|
|
634
|
-
}
|
|
635
|
-
return undefined;
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Clear the once-per-session grammar report gates when the degradation
|
|
639
|
-
* ledger's own generation has moved (#1536 review F5). Lazy
|
|
640
|
-
* compare-at-use-time against a monotonic counter, bumped by
|
|
641
|
-
* `resetDegradationLedger`, which `handleSessionStart` calls first thing —
|
|
642
|
-
* no listener, no retention. Every gate keyed to a SESSION rather than to
|
|
643
|
-
* the process lives here, so a new one cannot forget to re-arm: a gate that
|
|
644
|
-
* outlives the ledger it guards silently swallows the record it was only
|
|
645
|
-
* ever meant to de-duplicate (#1560 review F1).
|
|
646
|
-
*/
|
|
647
|
-
refreshGrammarSessionLatches() {
|
|
648
|
-
const ledgerGen = getDegradationLedgerGeneration();
|
|
649
|
-
if (ledgerGen === this.grammarNotificationsLedgerGen)
|
|
650
|
-
return;
|
|
651
|
-
this.grammarNotificationsLedgerGen = ledgerGen;
|
|
652
|
-
this.grammarLastNotifiedDelayMs.clear();
|
|
653
|
-
this.poisonedGrammarPaths.clear();
|
|
654
|
-
this.staleReportedGrammarPaths.clear();
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Log + record a grammar file on disk that isn't a wasm module, once per
|
|
658
|
-
* path per session. No user notification here: the caller goes on to attempt
|
|
659
|
-
* a re-download, and `recordGrammarFailure` is what interrupts the user if
|
|
660
|
-
* that also fails. A silent recovery should stay silent.
|
|
661
|
-
*/
|
|
662
|
-
reportPoisonedGrammarFile(candidate, grammarFile) {
|
|
663
|
-
// Per SESSION, not per process (#1560 review F1). The file is still
|
|
664
|
-
// poisoned after a session boundary and still degrades the language, so
|
|
665
|
-
// the new session's ledger has to carry the entry — a client-lifetime
|
|
666
|
-
// gate would leave `resetDegradationLedger` with a permanently empty
|
|
667
|
-
// grammar-blocked group and no record of why the language is degraded.
|
|
668
|
-
this.refreshGrammarSessionLatches();
|
|
669
|
-
if (this.poisonedGrammarPaths.has(candidate))
|
|
670
|
-
return;
|
|
671
|
-
this.poisonedGrammarPaths.add(candidate);
|
|
672
|
-
logTreeSitterDiagnostic({
|
|
673
|
-
subsystem: "tree-sitter-client",
|
|
674
|
-
level: "warn",
|
|
675
|
-
message: `ignoring ${candidate}: the file is not a wasm module (missing the \\0asm preamble) — ` +
|
|
676
|
-
`most likely a captive-portal or proxy page written by an earlier download (#1548). ` +
|
|
677
|
-
`pi-lens will treat the grammar as missing and try to fetch it again.`,
|
|
678
|
-
metadata: { grammarFile, path: candidate, outcome: "not-wasm" },
|
|
679
|
-
});
|
|
680
|
-
incrementDegradationCount({
|
|
681
|
-
kind: "grammar-blocked",
|
|
682
|
-
subject: grammarFile,
|
|
683
|
-
reason: "on-disk grammar file is not a wasm module — ignored, re-fetching",
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Is the cached grammar at `candidate` (whose wasm preamble already passed)
|
|
688
|
-
* still current against the pinned sha256 manifest (#1760)?
|
|
689
|
-
*
|
|
690
|
-
* A grammar downloaded once is never revisited when this repo bumps
|
|
691
|
-
* `TREE_SITTER_WASMS_VERSION` or changes a `SOURCE_OVERRIDES` entry — the
|
|
692
|
-
* cached file's name carries no version, so a stale build serves forever.
|
|
693
|
-
* The check is cheap and NEVER touches the network: it compares the
|
|
694
|
-
* on-disk sha256 (hashed once per `size:mtimeMs` stamp, memoized exactly
|
|
695
|
-
* like `verifiedGrammarPaths` above so a hot parse loop never re-hashes an
|
|
696
|
-
* unchanged file) against `pinnedGrammarHash`, which itself is a pure
|
|
697
|
-
* in-memory manifest lookup. A mismatch also catches on-disk corruption,
|
|
698
|
-
* which nothing detected before this.
|
|
699
|
-
*
|
|
700
|
-
* Vendored grammars (`VENDORED_GRAMMARS`) are skipped: they have no CDN
|
|
701
|
-
* pin to drift against, and their bytes are already guarded by the
|
|
702
|
-
* separate build-provenance check (`scripts/check-grammar-provenance.mjs`).
|
|
703
|
-
* No pinned hash for this filename (manifest missing, or a grammar added
|
|
704
|
-
* before `--write-manifest` was re-run) is treated as "can't verify" and
|
|
705
|
-
* trusted, the same fallback `downloadGrammarDetailed`'s own hash check
|
|
706
|
-
* already uses — never as a forced, unbounded refetch loop.
|
|
707
|
-
*/
|
|
708
|
-
isGrammarVersionCurrent(candidate, grammarFile, stamp) {
|
|
709
|
-
if (isVendoredGrammar(grammarFile))
|
|
710
|
-
return true;
|
|
711
|
-
if (this.staleGrammarVersionAt.get(candidate) === stamp) {
|
|
712
|
-
// #1801 review F1: the hash-verdict memo above is process-lifetime by
|
|
713
|
-
// design (re-hashing an unchanged file on every resolve is the exact
|
|
714
|
-
// cost this whole check exists to avoid), so an early return here must
|
|
715
|
-
// NOT skip the report — a still-stale grammar has to keep showing up
|
|
716
|
-
// in every NEW session's ledger, not just the session that first
|
|
717
|
-
// discovered the mismatch.
|
|
718
|
-
this.reportStaleGrammarVersion(candidate, grammarFile);
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
if (this.verifiedGrammarVersionAt.get(candidate) === stamp)
|
|
722
|
-
return true;
|
|
723
|
-
const pinnedHash = pinnedGrammarHash(grammarFile);
|
|
724
|
-
if (!pinnedHash) {
|
|
725
|
-
this.verifiedGrammarVersionAt.set(candidate, stamp);
|
|
726
|
-
return true;
|
|
727
|
-
}
|
|
728
|
-
const actualHash = grammarFileSha256(candidate);
|
|
729
|
-
if (actualHash === pinnedHash) {
|
|
730
|
-
this.verifiedGrammarVersionAt.set(candidate, stamp);
|
|
731
|
-
return true;
|
|
732
|
-
}
|
|
733
|
-
this.verifiedGrammarVersionAt.delete(candidate);
|
|
734
|
-
this.staleGrammarVersionAt.set(candidate, stamp);
|
|
735
|
-
this.reportStaleGrammarVersion(candidate, grammarFile);
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Log + record a version-stale (or corrupt) cached grammar, once per path
|
|
740
|
-
* per SESSION (#1801 review F1). Called from BOTH the memoized-stale path
|
|
741
|
-
* and the fresh-mismatch path in `isGrammarVersionCurrent`, so the report
|
|
742
|
-
* gate is independent of `staleGrammarVersionAt`'s own process-lifetime
|
|
743
|
-
* memo — a grammar that is STILL stale in a later session must still emit
|
|
744
|
-
* a fresh record into that session's ledger, exactly like
|
|
745
|
-
* `reportPoisonedGrammarFile` already does for the non-wasm case.
|
|
746
|
-
*/
|
|
747
|
-
reportStaleGrammarVersion(candidate, grammarFile) {
|
|
748
|
-
this.refreshGrammarSessionLatches();
|
|
749
|
-
if (this.staleReportedGrammarPaths.has(candidate))
|
|
750
|
-
return;
|
|
751
|
-
this.staleReportedGrammarPaths.add(candidate);
|
|
752
|
-
logTreeSitterDiagnostic({
|
|
753
|
-
subsystem: "tree-sitter-client",
|
|
754
|
-
level: "warn",
|
|
755
|
-
message: `ignoring ${candidate}: its sha256 no longer matches the pinned grammar ` +
|
|
756
|
-
`manifest — the pinned tree-sitter-wasms version (or a SOURCE_OVERRIDES entry) ` +
|
|
757
|
-
`moved since this file was downloaded. pi-lens will treat the grammar as ` +
|
|
758
|
-
`missing and re-fetch the current build (#1760).`,
|
|
759
|
-
metadata: { grammarFile, path: candidate, outcome: "stale-version" },
|
|
760
|
-
});
|
|
761
|
-
incrementDegradationCount({
|
|
762
|
-
kind: "grammar-blocked",
|
|
763
|
-
subject: grammarFile,
|
|
764
|
-
reason: "cached grammar no longer matches the pinned manifest hash — ignored, re-fetching",
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
/** Find tree-sitter grammar directory */
|
|
768
|
-
findGrammarsDir(deps = this.grammarDirResolutionDeps) {
|
|
769
|
-
const grammarsDir = deps.resolveAsset("grammars");
|
|
770
|
-
if (grammarsDir &&
|
|
771
|
-
fs.existsSync(path.join(grammarsDir, "tree-sitter-typescript.wasm"))) {
|
|
772
|
-
return grammarsDir;
|
|
773
|
-
}
|
|
774
|
-
// Fallback: a real `tree-sitter-wasms` package, if the user installed one
|
|
775
|
-
// (it is not a pi-lens dependency — grammars ship bundled / lazy-fetched).
|
|
776
|
-
try {
|
|
777
|
-
const wasmsOut = path.join(path.dirname(deps.resolvePackage("tree-sitter-wasms/package.json")), "out");
|
|
778
|
-
if (fs.existsSync(wasmsOut))
|
|
779
|
-
return wasmsOut;
|
|
780
|
-
}
|
|
781
|
-
catch {
|
|
782
|
-
/* fall through */
|
|
783
|
-
}
|
|
784
|
-
const cwdWasms = path.join(deps.cwd(), "node_modules", "tree-sitter-wasms", "out");
|
|
785
|
-
if (fs.existsSync(cwdWasms))
|
|
786
|
-
return cwdWasms;
|
|
787
|
-
return "";
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* The directory where grammars SHOULD live (web-tree-sitter/grammars),
|
|
791
|
-
* whether or not it exists yet — so we can create + populate it when the
|
|
792
|
-
* postinstall download was skipped (pnpm/bun). Returns undefined if
|
|
793
|
-
* web-tree-sitter itself can't be located.
|
|
794
|
-
*
|
|
795
|
-
* #3409: this used to resolve the BARE `web-tree-sitter` specifier and
|
|
796
|
-
* nothing else, which throws MODULE_NOT_FOUND inside the `bun
|
|
797
|
-
* build --compile` binary pi ships — so on every such host it returned
|
|
798
|
-
* undefined and NO non-core grammar could ever be fetched. It now shares the
|
|
799
|
-
* read path's ladder, whose first rung is a subpath that does resolve there.
|
|
800
|
-
*/
|
|
801
|
-
grammarsWriteDir() {
|
|
802
|
-
const pkgDir = this.webTreeSitterPackageDir();
|
|
803
|
-
return pkgDir ? path.join(pkgDir, "grammars") : undefined;
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Ensure a single grammar wasm is on disk, fetching it at runtime if the
|
|
807
|
-
* postinstall didn't (pnpm/bun skip lifecycle scripts — the documented
|
|
808
|
-
* build-scripts gap). Idempotent and de-duplicated per file. Best-effort:
|
|
809
|
-
* a failed fetch (e.g. offline) degrades to "grammar unavailable", never
|
|
810
|
-
* throws.
|
|
811
|
-
*/
|
|
812
|
-
async ensureGrammar(grammarFile) {
|
|
813
|
-
if (this.resolveGrammarFile(grammarFile)) {
|
|
814
|
-
return true;
|
|
815
|
-
}
|
|
816
|
-
if (!assertInstallAllowed(`tree-sitter grammar fetch: ${grammarFile}`)) {
|
|
817
|
-
const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable because the project is not trusted; ` +
|
|
818
|
-
`runtime grammar downloads are disabled until trust is granted.`;
|
|
819
|
-
logTreeSitterDiagnostic({
|
|
820
|
-
subsystem: "tree-sitter-client",
|
|
821
|
-
message: unavailable,
|
|
822
|
-
metadata: { grammarFile, outcome: "trust-gated" },
|
|
823
|
-
});
|
|
824
|
-
recordDegradation({
|
|
825
|
-
kind: "grammar-blocked",
|
|
826
|
-
subject: grammarFile,
|
|
827
|
-
reason: "runtime grammar download blocked because project is untrusted",
|
|
828
|
-
});
|
|
829
|
-
// Lazy clear-on-transition (#1363 review): compare the trust
|
|
830
|
-
// generation at use time -- no listener registration, no retention.
|
|
831
|
-
const generation = getProjectTrustGeneration();
|
|
832
|
-
if (generation !== this.trustNotificationsGeneration) {
|
|
833
|
-
this.trustNotificationsGeneration = generation;
|
|
834
|
-
this.trustBlockedGrammarNotifications.clear();
|
|
835
|
-
}
|
|
836
|
-
if (!this.trustBlockedGrammarNotifications.has(grammarFile)) {
|
|
837
|
-
this.trustBlockedGrammarNotifications.add(grammarFile);
|
|
838
|
-
notifyUserDegradation(`pi-lens: ${unavailable}`);
|
|
839
|
-
}
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
const inflight = this.grammarEnsurePromises.get(grammarFile);
|
|
843
|
-
if (inflight)
|
|
844
|
-
return inflight;
|
|
845
|
-
// A failed download only stays refused for its cooldown window (#1536):
|
|
846
|
-
// past that, fall through and retry rather than remembering an offline
|
|
847
|
-
// moment for the life of the process. No download is attempted while the
|
|
848
|
-
// cooldown is live, so a hard-down CDN is not re-hit on every parse. A
|
|
849
|
-
// DURABLE failure (grammarBlockDurable below) sets this to +Infinity,
|
|
850
|
-
// which reads the same as "still cooling down" here — it just never
|
|
851
|
-
// expires, matching the issue's "a genuinely unavailable grammar may
|
|
852
|
-
// still latch" allowance.
|
|
853
|
-
const retryAt = this.grammarRetryAtMs.get(grammarFile);
|
|
854
|
-
if (retryAt !== undefined && Date.now() < retryAt) {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
const task = (async () => {
|
|
858
|
-
try {
|
|
859
|
-
return await this.fetchGrammar(grammarFile);
|
|
860
|
-
}
|
|
861
|
-
catch (err) {
|
|
862
|
-
// Defensive (#1548): nothing in `fetchGrammar` throws today —
|
|
863
|
-
// `downloadGrammarDetailed` catches internally. But a thrown error
|
|
864
|
-
// here used to skip the whole `else` arm, so NO cooldown was armed
|
|
865
|
-
// and the rejection escaped through `task.finally` — the exact
|
|
866
|
-
// pre-#1536 shape (no cooldown, possible unhandled rejection). Funnel
|
|
867
|
-
// it into the same retryable failure path instead, so a future
|
|
868
|
-
// refactor that lets an exception out cannot regress to it.
|
|
869
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
870
|
-
this.recordGrammarFailure(grammarFile, `The runtime grammar fetch threw an unexpected error (${message}).`,
|
|
871
|
-
/* retryable */ true, "download-failed");
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
})();
|
|
875
|
-
this.grammarEnsurePromises.set(grammarFile, task);
|
|
876
|
-
// Evict on settle regardless of outcome (#1536): a resolved TRUE is
|
|
877
|
-
// superseded by resolveGrammarFile() finding the file on disk on the
|
|
878
|
-
// next call, and a resolved FALSE must not be remembered as the
|
|
879
|
-
// permanent verdict — only concurrent callers during the SAME in-flight
|
|
880
|
-
// attempt should ever observe this promise.
|
|
881
|
-
//
|
|
882
|
-
// The trailing `.catch` is UNFALSIFIABLE scaffolding, not verified
|
|
883
|
-
// defence (#1560 review F3): `finally` returns a DERIVED promise that
|
|
884
|
-
// rejects whenever `task` does, but the task's own try/catch above means
|
|
885
|
-
// it cannot reject, so no test can make this handler fire and removing it
|
|
886
|
-
// changes nothing today. It is here for the day the try/catch above stops
|
|
887
|
-
// being exhaustive — or the eviction callback itself throws — because at
|
|
888
|
-
// that point the fork becomes an unhandled rejection nobody is awaiting.
|
|
889
|
-
void task
|
|
890
|
-
.finally(() => {
|
|
891
|
-
if (this.grammarEnsurePromises.get(grammarFile) === task) {
|
|
892
|
-
this.grammarEnsurePromises.delete(grammarFile);
|
|
893
|
-
}
|
|
894
|
-
})
|
|
895
|
-
.catch(() => {
|
|
896
|
-
/* see above: the task's own catch is the real handler */
|
|
897
|
-
});
|
|
898
|
-
return task;
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* One grammar-fetch attempt: locate a writable dir, download, and record the
|
|
902
|
-
* outcome. Split out of `ensureGrammar` so the whole body sits under one
|
|
903
|
-
* try/catch that funnels any throw into `recordGrammarFailure` (#1548).
|
|
904
|
-
*/
|
|
905
|
-
async fetchGrammar(grammarFile) {
|
|
906
|
-
// BEFORE resolving a write directory: a vendored grammar is never
|
|
907
|
-
// downloaded, so the write dir has no bearing on its verdict. Resolving
|
|
908
|
-
// first let the "no writable grammars directory" branch below answer
|
|
909
|
-
// RETRYABLE for a missing vendored wasm on any host where
|
|
910
|
-
// web-tree-sitter isn't locatable — reporting a packaging fault as a
|
|
911
|
-
// transient download failure, and arming a cooldown for a fetch that can
|
|
912
|
-
// never happen.
|
|
913
|
-
if (isVendoredGrammar(grammarFile)) {
|
|
914
|
-
const { reason } = vendoredGrammarRefusal(grammarFile);
|
|
915
|
-
this.recordGrammarFailure(grammarFile, reason ?? `${grammarFile} is missing from vendor/grammars/.`,
|
|
916
|
-
/* retryable */ false);
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
const dir = this.grammarsDir && fs.existsSync(this.grammarsDir)
|
|
920
|
-
? this.grammarsDir
|
|
921
|
-
: this.grammarsWriteDir();
|
|
922
|
-
if (!dir) {
|
|
923
|
-
// Nothing to write into: web-tree-sitter is not locatable by ANY rung
|
|
924
|
-
// of the shared ladder (this runtime's resolver, pi-lens's own package
|
|
925
|
-
// root, the working directory), so the package is missing from this
|
|
926
|
-
// install. Still an environment condition, not a CDN verdict — #1536
|
|
927
|
-
// review F2: it gets the same cooldown + notification treatment as a
|
|
928
|
-
// download failure rather than a silent unmemoized `false` that re-does
|
|
929
|
-
// the same failing resolution sweep on every demand.
|
|
930
|
-
//
|
|
931
|
-
// #3409: before the shared ladder, this arm ALSO caught every host
|
|
932
|
-
// whose runtime cannot resolve a bare specifier — where the package was
|
|
933
|
-
// present and the wasm downloadable — and told the user to approve
|
|
934
|
-
// build scripts or restore network access. Hence the cause.
|
|
935
|
-
this.recordGrammarFailure(grammarFile, "No grammars directory could be resolved for the runtime fetch: the web-tree-sitter package is not locatable from this runtime, from pi-lens's own package root, or from the working directory.",
|
|
936
|
-
/* retryable */ true, "write-dir-unresolvable");
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
// A destination that cannot be written to is not a download problem
|
|
940
|
-
// either (#3409): a root-owned `node_modules` under a global install
|
|
941
|
-
// reaches `downloadGrammarDetailed`'s catch-all, which reports no reason
|
|
942
|
-
// at all, so the user was sent to inspect their package manager and
|
|
943
|
-
// network for an EACCES.
|
|
944
|
-
const writeBlock = grammarWriteBlockedReason(dir);
|
|
945
|
-
if (writeBlock) {
|
|
946
|
-
this.recordGrammarFailure(grammarFile, `The grammars directory ${dir} cannot be written to (${writeBlock}), so the runtime fetch has nowhere to land.`,
|
|
947
|
-
/* retryable */ true, "write-dir-unwritable");
|
|
948
|
-
return false;
|
|
949
|
-
}
|
|
950
|
-
// Reuse the shared single-file downloader (same CDN/source as the
|
|
951
|
-
// postinstall) — see clients/grammar-source.ts.
|
|
952
|
-
const { ok, retryable, reason } = await downloadGrammarDetailed(dir, grammarFile);
|
|
953
|
-
if (ok) {
|
|
954
|
-
if (!this.grammarsDir)
|
|
955
|
-
this.grammarsDir = dir;
|
|
956
|
-
this.grammarRetryAtMs.delete(grammarFile);
|
|
957
|
-
this.grammarFailureAttempts.delete(grammarFile);
|
|
958
|
-
this.grammarLastNotifiedDelayMs.delete(grammarFile);
|
|
959
|
-
logTreeSitterDiagnostic({
|
|
960
|
-
subsystem: "tree-sitter-client",
|
|
961
|
-
level: "warn",
|
|
962
|
-
message: `fetched missing tree-sitter grammar ${grammarFile} at runtime (install scripts were skipped by the package manager)`,
|
|
963
|
-
metadata: { grammarFile, outcome: "fetched" },
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
// `retryable` distinguishes a durable CDN verdict (404/410 — a
|
|
968
|
-
// retry hits the same answer) from everything else (offline, DNS,
|
|
969
|
-
// a down CDN, a 5xx, a timeout), which says nothing durable about
|
|
970
|
-
// the grammar (#1536 review F4). Only the retryable case gets the
|
|
971
|
-
// bounded cooldown; a durable failure keeps the pre-#1536 latched
|
|
972
|
-
// behavior, matching the issue's own allowance.
|
|
973
|
-
//
|
|
974
|
-
// `reason`, when the downloader supplies one, names the actual failure
|
|
975
|
-
// shape (#1548: "returned an HTML page, not a wasm module") instead of
|
|
976
|
-
// the generic guess — the difference between a diagnosable log line
|
|
977
|
-
// and one that sends the user to inspect their package manager while a
|
|
978
|
-
// captive portal is the real cause.
|
|
979
|
-
this.recordGrammarFailure(grammarFile, reason ??
|
|
980
|
-
"The package manager skipped install scripts and the runtime download failed.", retryable, "download-failed");
|
|
981
|
-
}
|
|
982
|
-
return ok;
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Record one failed grammar-ensure attempt: arm (or extend) the retry
|
|
986
|
-
* cooldown, record the degradation, and notify the user at most once per
|
|
987
|
-
* session per DISTINCT retry delay (#1536 review F1/F2/F4/F5/F6). Shared
|
|
988
|
-
* by the "no writable directory" and "download failed" arms of
|
|
989
|
-
* `ensureGrammar` — both are failures of the SAME ensure attempt, just
|
|
990
|
-
* with a different point of failure.
|
|
991
|
-
*
|
|
992
|
-
* `cause` is that point of failure (#3409). It picks the REMEDY the user is
|
|
993
|
-
* told and lands in the durable record, so `~/.pi-lens` forensics can tell
|
|
994
|
-
* "this host has nowhere to write" from "this download failed" — the two
|
|
995
|
-
* were one string, and the one it carried named build scripts and the
|
|
996
|
-
* network for a cause that is neither.
|
|
997
|
-
*/
|
|
998
|
-
recordGrammarFailure(grammarFile, detail, retryable, cause) {
|
|
999
|
-
const attempts = (this.grammarFailureAttempts.get(grammarFile) ?? 0) + 1;
|
|
1000
|
-
this.grammarFailureAttempts.set(grammarFile, attempts);
|
|
1001
|
-
// A durable verdict (404/410) never expires -- matches the issue's "a
|
|
1002
|
-
// genuinely unavailable grammar may still latch" allowance. Everything
|
|
1003
|
-
// else gets the bounded exponential cooldown.
|
|
1004
|
-
const retryDelayMs = retryable
|
|
1005
|
-
? transientRetryDelayMs(attempts, "probe-timeout")
|
|
1006
|
-
: undefined;
|
|
1007
|
-
this.grammarRetryAtMs.set(grammarFile, retryDelayMs === undefined
|
|
1008
|
-
? Number.POSITIVE_INFINITY
|
|
1009
|
-
: Date.now() + retryDelayMs);
|
|
1010
|
-
// The remedy the user is given, keyed to WHERE the attempt failed. The
|
|
1011
|
-
// last two arms are the pre-existing strings, byte for byte; the first
|
|
1012
|
-
// two exist because sending someone to `pnpm approve-builds` and their
|
|
1013
|
-
// network for a resolution or permission fault is the #3409 complaint.
|
|
1014
|
-
const remedy = cause === "write-dir-unresolvable"
|
|
1015
|
-
? `This is a packaging/module-resolution fault, not a download problem: reinstall pi-lens so its ` +
|
|
1016
|
-
`web-tree-sitter dependency is present, or drop the wasm into pi-lens's own grammars/ directory.`
|
|
1017
|
-
: cause === "write-dir-unwritable"
|
|
1018
|
-
? `This is a filesystem-permission fault, not a download problem: make that directory writable ` +
|
|
1019
|
-
`for the user pi-lens runs as, or reinstall pi-lens as that user.`
|
|
1020
|
-
: retryable
|
|
1021
|
-
? `if the problem persists, allow the package manager's build scripts ` +
|
|
1022
|
-
`(pnpm approve-builds / bun trustedDependencies) or restore network access.`
|
|
1023
|
-
: `Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
|
|
1024
|
-
`(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
|
|
1025
|
-
const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
|
|
1026
|
-
`module reports and structural rules for this language will be degraded. ` +
|
|
1027
|
-
`${detail} ` +
|
|
1028
|
-
(retryable
|
|
1029
|
-
? `pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; `
|
|
1030
|
-
: `The grammar source reports it does not exist (not a network problem), so this will not ` +
|
|
1031
|
-
`resolve on retry. `) +
|
|
1032
|
-
remedy;
|
|
1033
|
-
logTreeSitterDiagnostic({
|
|
1034
|
-
subsystem: "tree-sitter-client",
|
|
1035
|
-
message: unavailable,
|
|
1036
|
-
metadata: {
|
|
1037
|
-
grammarFile,
|
|
1038
|
-
outcome: "unavailable",
|
|
1039
|
-
retryable,
|
|
1040
|
-
retryDelayMs,
|
|
1041
|
-
attempts,
|
|
1042
|
-
// Omitted, not defaulted, for the arms that are neither a fetch
|
|
1043
|
-
// destination nor a download verdict (a vendored grammar is never
|
|
1044
|
-
// downloaded; a decode failure happened AFTER a successful one), so
|
|
1045
|
-
// those records keep their pre-#3409 shape rather than carrying a
|
|
1046
|
-
// cause that is not true of them.
|
|
1047
|
-
...(cause ? { cause } : {}),
|
|
1048
|
-
},
|
|
1049
|
-
});
|
|
1050
|
-
// #1536 review F1: incrementDegradationCount keeps ONE ring-buffer slot
|
|
1051
|
-
// per subject (bumping its count in place) instead of recordDegradation's
|
|
1052
|
-
// one-new-entry-per-call — a grammar retrying every cooldown window would
|
|
1053
|
-
// otherwise flood the shared "grammar-blocked" kind's 20-slot ring and
|
|
1054
|
-
// evict unrelated entries (e.g. a different grammar's V8-crash block
|
|
1055
|
-
// reason from loadLanguage/grammarBlockReason) after ~20 cycles.
|
|
1056
|
-
incrementDegradationCount({
|
|
1057
|
-
kind: "grammar-blocked",
|
|
1058
|
-
subject: grammarFile,
|
|
1059
|
-
// #1548: carry `detail` into the RETRYABLE reason too. All retryable
|
|
1060
|
-
// failures used to share one string, so the ledger could not tell an
|
|
1061
|
-
// offline laptop from a captive portal serving HTML — the two need
|
|
1062
|
-
// different user actions, and the ledger is what a bug report shows.
|
|
1063
|
-
reason: retryable
|
|
1064
|
-
? `runtime grammar download failed — retryable (${detail.trim()})`
|
|
1065
|
-
: `runtime grammar download failed — durable (${detail.trim()})`,
|
|
1066
|
-
});
|
|
1067
|
-
this.refreshGrammarSessionLatches();
|
|
1068
|
-
// HUMAN-audience: an offline grammar fetch silently degrades this
|
|
1069
|
-
// language's features, so it reaches the user through the HOST's render
|
|
1070
|
-
// path (#1333) rather than a raw terminal write. Notify once per DISTINCT
|
|
1071
|
-
// retry delay this session (#1536 review F6) -- a fresh failure streak or
|
|
1072
|
-
// an escalated backoff is new information worth surfacing; an unchanged
|
|
1073
|
-
// repeat during the same cooldown tier is not.
|
|
1074
|
-
const notifyKey = retryDelayMs ?? -1;
|
|
1075
|
-
if (this.grammarLastNotifiedDelayMs.get(grammarFile) !== notifyKey) {
|
|
1076
|
-
this.grammarLastNotifiedDelayMs.set(grammarFile, notifyKey);
|
|
1077
|
-
notifyUserDegradation(`pi-lens: ${unavailable}`);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Record a `Language.load` failure on a file `resolveGrammarFile` had just
|
|
1082
|
-
* vouched for (#1564) — the diagnosis gap #1548 left open: the preamble
|
|
1083
|
-
* check proves the first four bytes are `\0asm`, not that the whole body
|
|
1084
|
-
* decodes. A decode error here (a truncated download, on-disk corruption)
|
|
1085
|
-
* is evidence the FILE is bad, so it gets the exact same treatment as an
|
|
1086
|
-
* ensure-time download failure: invalidate the resolve memo so the next
|
|
1087
|
-
* demand re-fetches instead of reusing the same broken file forever, and
|
|
1088
|
-
* reuse `recordGrammarFailure`'s cooldown/degradation/notification
|
|
1089
|
-
* machinery rather than hand-rolling a parallel one — a persistently
|
|
1090
|
-
* truncated CDN response would otherwise re-download and re-fail on every
|
|
1091
|
-
* single parse.
|
|
1092
|
-
*
|
|
1093
|
-
* A DURABLE decode failure (the bytes are complete and correct, but ABI-
|
|
1094
|
-
* incompatible with the installed `web-tree-sitter` — a version drift, not
|
|
1095
|
-
* a truncation) is classified `retryable` here too, same as everything
|
|
1096
|
-
* else this method sees: there is no way to tell "truncated" from "wrong
|
|
1097
|
-
* ABI" from the error message alone, so it re-downloads and re-fails once
|
|
1098
|
-
* per cooldown tier instead of latching forever. That is bounded by the
|
|
1099
|
-
* cooldown ladder (this never re-fetches faster than #1536's backoff) and
|
|
1100
|
-
* by `BLOCKED_GRAMMARS` for the narrower case that's fatal to the process
|
|
1101
|
-
* rather than just wrong (`grammar-source.ts`). The redownload only helps
|
|
1102
|
-
* if a fresh fetch could actually be a DIFFERENT (compatible) build,
|
|
1103
|
-
* which is exactly what the `grammar-source.test.ts` version/lock sync
|
|
1104
|
-
* guard (`lock.version === TREE_SITTER_WASMS_VERSION`) exists to keep
|
|
1105
|
-
* true — without it, a version bump with a stale lock would make every
|
|
1106
|
-
* redownload land the identical bytes and spin the ladder against a file
|
|
1107
|
-
* that can never pass.
|
|
1108
|
-
*/
|
|
1109
|
-
recordGrammarLoadFailure(grammarPath, grammarFile, err) {
|
|
1110
|
-
const stamp = grammarFileStamp(grammarPath);
|
|
1111
|
-
this.verifiedGrammarPaths.delete(grammarPath);
|
|
1112
|
-
if (stamp)
|
|
1113
|
-
this.decodeFailedGrammarPaths.set(grammarPath, stamp);
|
|
1114
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1115
|
-
logTreeSitterDiagnostic({
|
|
1116
|
-
subsystem: "tree-sitter-client",
|
|
1117
|
-
level: "warn",
|
|
1118
|
-
message: `Language.load failed for ${grammarPath} even though the wasm preamble check ` +
|
|
1119
|
-
`passed (${message}) — most likely a truncated download (#1564). Treating the ` +
|
|
1120
|
-
`grammar as missing so the next demand re-fetches it.`,
|
|
1121
|
-
metadata: { grammarFile, path: grammarPath, outcome: "load-failed" },
|
|
1122
|
-
});
|
|
1123
|
-
this.recordGrammarFailure(grammarFile, `Language.load failed on a resolved grammar file (${message}).`,
|
|
1124
|
-
/* retryable */ true);
|
|
1125
|
-
}
|
|
1126
|
-
/** Initialize tree-sitter WASM runtime */
|
|
1127
|
-
async init() {
|
|
1128
|
-
if (this.wasmAborted)
|
|
1129
|
-
return false;
|
|
1130
|
-
if (this.initialized)
|
|
1131
|
-
return true;
|
|
1132
|
-
if (this.webTreeSitterLoadFailed)
|
|
1133
|
-
return false;
|
|
1134
|
-
if (this.initPromise)
|
|
1135
|
-
return this.initPromise;
|
|
1136
|
-
this.initPromise = (async () => {
|
|
1137
|
-
try {
|
|
1138
|
-
let mod;
|
|
1139
|
-
try {
|
|
1140
|
-
mod = await loadWebTreeSitter();
|
|
1141
|
-
}
|
|
1142
|
-
catch (err) {
|
|
1143
|
-
// See webTreeSitterLoadFailed's doc: an EVALUATION-shaped
|
|
1144
|
-
// rejection is a dead retry, so latch instead of leaving
|
|
1145
|
-
// initPromise clearable (the finally below would otherwise let
|
|
1146
|
-
// the very next parse call re-import the same doomed URL). A
|
|
1147
|
-
// RESOLUTION-shaped rejection is left alone — initPromise still
|
|
1148
|
-
// clears below, so the next init() call retries for real.
|
|
1149
|
-
const classification = classifyWebTreeSitterLoadFailure(err);
|
|
1150
|
-
if (classification === "evaluation") {
|
|
1151
|
-
this.webTreeSitterLoadFailed = true;
|
|
1152
|
-
}
|
|
1153
|
-
recordDegradationOnce({
|
|
1154
|
-
kind: "web-tree-sitter-load-failed",
|
|
1155
|
-
subject: "web-tree-sitter",
|
|
1156
|
-
reason: (err instanceof Error ? err.message : String(err)) +
|
|
1157
|
-
(classification === "resolution"
|
|
1158
|
-
? " (resolution failure — retryable)"
|
|
1159
|
-
: " (evaluation failure — latched for the session)"),
|
|
1160
|
-
});
|
|
1161
|
-
throw err;
|
|
1162
|
-
}
|
|
1163
|
-
// biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter module shape varies (Parser direct / default-wrapped)
|
|
1164
|
-
const anyMod = mod;
|
|
1165
|
-
const ParserClass = anyMod.Parser || anyMod.default || anyMod;
|
|
1166
|
-
if (!ParserClass || typeof ParserClass.init !== "function") {
|
|
1167
|
-
this.dbg("Parser class not found or missing init method");
|
|
1168
|
-
return false;
|
|
1169
|
-
}
|
|
1170
|
-
// biome-ignore lint/suspicious/noExplicitAny: Parser class type
|
|
1171
|
-
this.ParserClass = ParserClass;
|
|
1172
|
-
// Store Language loader from module (not from Parser)
|
|
1173
|
-
this.LanguageLoader = mod.Language;
|
|
1174
|
-
// Resolve WASM path using same multi-strategy helper (hoisted installs +
|
|
1175
|
-
// on-the-fly compilation by pi).
|
|
1176
|
-
const wasmPath = this.resolveWebTreeSitterAsset("tree-sitter.wasm");
|
|
1177
|
-
if (!wasmPath) {
|
|
1178
|
-
this.dbg("Could not resolve tree-sitter.wasm");
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
const wasmDir = path.dirname(wasmPath);
|
|
1182
|
-
this.dbg(`Looking for WASM at: ${wasmPath}, exists: ${fs.existsSync(wasmPath)}`);
|
|
1183
|
-
await ParserClass.init({
|
|
1184
|
-
locateFile: (scriptName) => {
|
|
1185
|
-
const fullPath = path.join(wasmDir, scriptName);
|
|
1186
|
-
this.dbg(`locateFile: ${scriptName} -> ${fullPath}`);
|
|
1187
|
-
return fullPath;
|
|
1188
|
-
},
|
|
1189
|
-
});
|
|
1190
|
-
this.initialized = true;
|
|
1191
|
-
return true;
|
|
1192
|
-
}
|
|
1193
|
-
catch (err) {
|
|
1194
|
-
this.reportWasmAbort(err);
|
|
1195
|
-
this.dbg(`Init error: ${err}`);
|
|
1196
|
-
return false;
|
|
1197
|
-
}
|
|
1198
|
-
finally {
|
|
1199
|
-
this.initPromise = null;
|
|
1200
|
-
}
|
|
1201
|
-
})();
|
|
1202
|
-
return this.initPromise;
|
|
1203
|
-
}
|
|
1204
|
-
/**
|
|
1205
|
-
* Re-arm the web-tree-sitter load latch for a new session (#1592 review
|
|
1206
|
-
* round 2 F2). Deliberately does NOT touch `wasmAborted` or `initialized`
|
|
1207
|
-
* — see `webTreeSitterLoadFailed`'s doc for why the abort case stays
|
|
1208
|
-
* process-lifetime, and a successful init has nothing to re-arm. Called
|
|
1209
|
-
* from `resetTreeSitterClientLoadState()` (tree-sitter-shared.ts), wired
|
|
1210
|
-
* into `resetDispatchBaselines()` beside `resetAstGrepNapiLoadState()`.
|
|
1211
|
-
*/
|
|
1212
|
-
resetLoadStateForSession() {
|
|
1213
|
-
this.webTreeSitterLoadFailed = false;
|
|
1214
|
-
}
|
|
1215
|
-
/** Load language grammar */
|
|
1216
|
-
async loadLanguage(languageId) {
|
|
1217
|
-
if (this.wasmAborted)
|
|
1218
|
-
return null;
|
|
1219
|
-
this.dbg(`Loading language: ${languageId}`);
|
|
1220
|
-
if (this.languages.has(languageId)) {
|
|
1221
|
-
this.dbg(`Language ${languageId} already loaded`);
|
|
1222
|
-
return this.languages.get(languageId);
|
|
1223
|
-
}
|
|
1224
|
-
if (!this.ParserClass) {
|
|
1225
|
-
this.dbg(`ParserClass not initialized`);
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
const grammarFile = LANGUAGE_TO_GRAMMAR[languageId];
|
|
1229
|
-
if (!grammarFile) {
|
|
1230
|
-
this.dbg(`No grammar file for ${languageId}`);
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
// A grammar that fatally crashes this runtime (uncatchable V8 abort) must
|
|
1234
|
-
// never be loaded — skip it and degrade to "unavailable" (#423/#432). The
|
|
1235
|
-
// grammar-health nightly is what decides membership of BLOCKED_GRAMMARS.
|
|
1236
|
-
const blockReason = grammarBlockReason(grammarFile);
|
|
1237
|
-
if (blockReason) {
|
|
1238
|
-
this.dbg(`Grammar ${grammarFile} blocked on this runtime — ${blockReason}`);
|
|
1239
|
-
recordDegradation({
|
|
1240
|
-
kind: "grammar-blocked",
|
|
1241
|
-
subject: grammarFile,
|
|
1242
|
-
reason: blockReason,
|
|
1243
|
-
});
|
|
1244
|
-
return null;
|
|
1245
|
-
}
|
|
1246
|
-
// Look across the bundled core `grammars/` dir and the postinstall/lazy
|
|
1247
|
-
// dir. Lazily fetch only if the grammar is in neither (pnpm/bun skip
|
|
1248
|
-
// postinstall; the long-tail grammars aren't bundled). Only the language
|
|
1249
|
-
// actually being parsed is fetched.
|
|
1250
|
-
let grammarPath = this.resolveGrammarFile(grammarFile);
|
|
1251
|
-
if (!grammarPath) {
|
|
1252
|
-
if (await this.ensureGrammar(grammarFile)) {
|
|
1253
|
-
grammarPath = this.resolveGrammarFile(grammarFile);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
this.dbg(`Grammar path: ${grammarPath}, exists: ${grammarPath && fs.existsSync(grammarPath)}`);
|
|
1257
|
-
if (!grammarPath || !fs.existsSync(grammarPath)) {
|
|
1258
|
-
this.dbg(`Grammar file not found: ${grammarPath}`);
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
try {
|
|
1262
|
-
if (!this.LanguageLoader?.load) {
|
|
1263
|
-
this.dbg(`LanguageLoader.load not available`);
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
this.dbg(`Calling Language.load...`);
|
|
1267
|
-
const language = await this.LanguageLoader.load(grammarPath);
|
|
1268
|
-
this.dbg(`Language loaded: ${language?.name || "unknown"}`);
|
|
1269
|
-
if (language) {
|
|
1270
|
-
this.languages.set(languageId, language);
|
|
1271
|
-
}
|
|
1272
|
-
return language;
|
|
1273
|
-
}
|
|
1274
|
-
catch (err) {
|
|
1275
|
-
// An uncatchable-shaped WASM abort poisons the whole process (#402) —
|
|
1276
|
-
// `reportWasmAbort` already records + handles it. Everything else
|
|
1277
|
-
// (#1564) is a DECODE failure on a file `resolveGrammarFile` just
|
|
1278
|
-
// vouched for — e.g. "Code section extends past end of the module" on
|
|
1279
|
-
// a truncated-but-magic-valid download. That is evidence the file
|
|
1280
|
-
// itself is bad, not the runtime, so record it and stop vouching for
|
|
1281
|
-
// this exact file so the next demand can re-fetch it.
|
|
1282
|
-
if (!this.reportWasmAbort(err)) {
|
|
1283
|
-
this.recordGrammarLoadFailure(grammarPath, grammarFile, err);
|
|
1284
|
-
}
|
|
1285
|
-
this.dbg(`Language load error: ${err}`);
|
|
1286
|
-
return null;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
/** Get or create parser for a language */
|
|
1290
|
-
async getParser(languageId) {
|
|
1291
|
-
if (this.wasmAborted)
|
|
1292
|
-
return null;
|
|
1293
|
-
if (this.parsers.has(languageId)) {
|
|
1294
|
-
return this.parsers.get(languageId);
|
|
1295
|
-
}
|
|
1296
|
-
const language = await this.loadLanguage(languageId);
|
|
1297
|
-
if (!language || !this.ParserClass)
|
|
1298
|
-
return null;
|
|
1299
|
-
const parser = new this.ParserClass();
|
|
1300
|
-
parser.setLanguage(language);
|
|
1301
|
-
this.parsers.set(languageId, parser);
|
|
1302
|
-
return parser;
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Parse a file and return the AST tree. The tree stays valid only until the
|
|
1306
|
-
* caller's next `await` — prefer `withParsedTree`, which extracts inside the
|
|
1307
|
-
* cache-safe window (#417/#675).
|
|
1308
|
-
*/
|
|
1309
|
-
async parseFile(filePath, languageId, contentOverride) {
|
|
1310
|
-
const outcome = await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => tree);
|
|
1311
|
-
return outcome.parsed ? outcome.value : null;
|
|
1312
|
-
}
|
|
1313
|
-
async withParsedTree(filePath, languageId, contentOverride, consume) {
|
|
1314
|
-
return this.parseFileAndUse(filePath, languageId, contentOverride, consume);
|
|
1315
|
-
}
|
|
1316
|
-
async parseFileAndUse(filePath, languageId, contentOverride, consume) {
|
|
1317
|
-
this.dbg(`Parsing ${filePath} with language ${languageId}`);
|
|
1318
|
-
const parser = await this.getParser(languageId);
|
|
1319
|
-
if (!parser) {
|
|
1320
|
-
this.dbg(`Failed to get parser for ${languageId}`);
|
|
1321
|
-
return NOT_PARSED;
|
|
1322
|
-
}
|
|
1323
|
-
let tree;
|
|
1324
|
-
try {
|
|
1325
|
-
const content = contentOverride ?? fs.readFileSync(filePath, "utf-8");
|
|
1326
|
-
this.dbg(`File content length: ${content.length}`);
|
|
1327
|
-
const cachedTree = this.treeCache.get(filePath, content, languageId);
|
|
1328
|
-
if (cachedTree) {
|
|
1329
|
-
this.dbg(`Using cached tree for ${filePath}`);
|
|
1330
|
-
tree = cachedTree;
|
|
1331
|
-
}
|
|
1332
|
-
else {
|
|
1333
|
-
const parseStartedAt = performance.now();
|
|
1334
|
-
this.recordParserCounter("parserInvocations");
|
|
1335
|
-
try {
|
|
1336
|
-
tree = parser.parse(content);
|
|
1337
|
-
}
|
|
1338
|
-
catch (err) {
|
|
1339
|
-
this.recordParserCounter("parserFailures");
|
|
1340
|
-
throw err;
|
|
1341
|
-
}
|
|
1342
|
-
finally {
|
|
1343
|
-
this.recordParserCounter("parserDurationMs", performance.now() - parseStartedAt);
|
|
1344
|
-
}
|
|
1345
|
-
this.dbg(`Parsed, root node type: ${tree.rootNode.type}`);
|
|
1346
|
-
this.treeCache.set(filePath, content, languageId, tree);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
catch (err) {
|
|
1350
|
-
this.reportWasmAbort(err);
|
|
1351
|
-
this.dbg(`Parse error: ${err}`);
|
|
1352
|
-
return NOT_PARSED;
|
|
1353
|
-
}
|
|
1354
|
-
try {
|
|
1355
|
-
return { parsed: true, value: consume(tree) };
|
|
1356
|
-
}
|
|
1357
|
-
catch (error) {
|
|
1358
|
-
this.reportWasmAbort(error);
|
|
1359
|
-
throw error;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Detect and extract injected content from template literals
|
|
1364
|
-
* Used for security analysis (SQL injection, unsafe regex, etc.)
|
|
1365
|
-
*/
|
|
1366
|
-
extractInjections(filePath, content) {
|
|
1367
|
-
const injections = [];
|
|
1368
|
-
// Pattern: sql`SELECT * FROM users` or query`...`
|
|
1369
|
-
const sqlPattern = /\b(sql|query|execute)\s*`([^`]+)`/gi;
|
|
1370
|
-
let match;
|
|
1371
|
-
while ((match = sqlPattern.exec(content)) !== null) {
|
|
1372
|
-
const lines = content.slice(0, match.index).split("\n");
|
|
1373
|
-
injections.push({
|
|
1374
|
-
type: "sql",
|
|
1375
|
-
content: match[2],
|
|
1376
|
-
line: lines.length,
|
|
1377
|
-
column: lines[lines.length - 1].length,
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
// Pattern: styled.div`color: red;` or css`...`
|
|
1381
|
-
const cssPattern = /\b(styled(?:\.\w+)?|css)\s*`([^`]+)`/gi;
|
|
1382
|
-
while ((match = cssPattern.exec(content)) !== null) {
|
|
1383
|
-
const lines = content.slice(0, match.index).split("\n");
|
|
1384
|
-
injections.push({
|
|
1385
|
-
type: "css",
|
|
1386
|
-
content: match[2],
|
|
1387
|
-
line: lines.length,
|
|
1388
|
-
column: lines[lines.length - 1].length,
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
// Pattern: new RegExp(`pattern`)
|
|
1392
|
-
const regexPattern = /new\s+RegExp\s*\(\s*`([^`]+)`/gi;
|
|
1393
|
-
while ((match = regexPattern.exec(content)) !== null) {
|
|
1394
|
-
const lines = content.slice(0, match.index).split("\n");
|
|
1395
|
-
injections.push({
|
|
1396
|
-
type: "regex",
|
|
1397
|
-
content: match[1],
|
|
1398
|
-
line: lines.length,
|
|
1399
|
-
column: lines[lines.length - 1].length,
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
this.dbg(`Found ${injections.length} injections in ${filePath}`);
|
|
1403
|
-
return injections;
|
|
1404
|
-
}
|
|
1405
|
-
/** Check if tree-sitter is available (a core grammar resolves somewhere). */
|
|
1406
|
-
isAvailable() {
|
|
1407
|
-
if (this.wasmAborted)
|
|
1408
|
-
return false;
|
|
1409
|
-
// Available if the core TS grammar resolves in ANY source dir — the bundled
|
|
1410
|
-
// `grammars/` counts even when web-tree-sitter/grammars is empty (no
|
|
1411
|
-
// postinstall on pnpm/bun, or a fresh CI checkout).
|
|
1412
|
-
if (this.resolveGrammarFile("tree-sitter-typescript.wasm"))
|
|
1413
|
-
return true;
|
|
1414
|
-
// Re-evaluate the legacy dir in case grammars were installed after start.
|
|
1415
|
-
const dir = this.findGrammarsDir();
|
|
1416
|
-
this.grammarsDir = dir;
|
|
1417
|
-
return !!dir && fs.existsSync(dir);
|
|
1418
|
-
}
|
|
1419
|
-
/** Check if specific language is supported */
|
|
1420
|
-
async isLanguageSupported(languageId) {
|
|
1421
|
-
if (this.wasmAborted)
|
|
1422
|
-
return false;
|
|
1423
|
-
if (!this.initialized)
|
|
1424
|
-
await this.init();
|
|
1425
|
-
const language = await this.loadLanguage(languageId);
|
|
1426
|
-
return language !== null;
|
|
1427
|
-
}
|
|
1428
|
-
/** Get loaded language for symbol extraction */
|
|
1429
|
-
getLanguage(languageId) {
|
|
1430
|
-
if (this.wasmAborted)
|
|
1431
|
-
return null;
|
|
1432
|
-
return this.languages.get(languageId) || null;
|
|
1433
|
-
}
|
|
1434
|
-
// --- Structural Search ---
|
|
1435
|
-
/**
|
|
1436
|
-
* Search for a structural pattern in files
|
|
1437
|
-
*
|
|
1438
|
-
* @param pattern - Pattern with metavariables (e.g., "console.log($MSG)")
|
|
1439
|
-
* @param languageId - Language ID (typescript, python, etc.)
|
|
1440
|
-
* @param rootDir - Directory to search
|
|
1441
|
-
* @param options - Search options
|
|
1442
|
-
* @returns Array of matches with captures
|
|
1443
|
-
*/
|
|
1444
|
-
async structuralSearch(pattern, languageId, rootDir, options = {}) {
|
|
1445
|
-
if (!this.initialized) {
|
|
1446
|
-
const ok = await this.init();
|
|
1447
|
-
if (!ok)
|
|
1448
|
-
return [];
|
|
1449
|
-
}
|
|
1450
|
-
try {
|
|
1451
|
-
await this.queryLoader.loadQueries(rootDir);
|
|
1452
|
-
}
|
|
1453
|
-
catch (err) {
|
|
1454
|
-
this.dbg(`Failed to load queries for ${rootDir}: ${err}`);
|
|
1455
|
-
}
|
|
1456
|
-
// Compile pattern into tree-sitter query
|
|
1457
|
-
this.dbg(`Compiling pattern: ${pattern.slice(0, 50)}...`);
|
|
1458
|
-
const compiled = await this.compileQuery(pattern, languageId);
|
|
1459
|
-
if (!compiled) {
|
|
1460
|
-
this.dbg(`Pattern compilation failed`);
|
|
1461
|
-
return [];
|
|
1462
|
-
}
|
|
1463
|
-
this.dbg(`Pattern compiled, metavars: ${compiled.metavars.join(", ")}`);
|
|
1464
|
-
// Collect source files
|
|
1465
|
-
const files = this.collectFiles(rootDir, languageId, options.fileFilter);
|
|
1466
|
-
this.dbg(`Scanning ${files.length} files...`);
|
|
1467
|
-
const matches = [];
|
|
1468
|
-
const maxResults = options.maxResults ?? 50;
|
|
1469
|
-
for (const file of files) {
|
|
1470
|
-
if (matches.length >= maxResults)
|
|
1471
|
-
break;
|
|
1472
|
-
const fileMatches = await this.searchFileWithQuery(file, compiled.query, compiled.metavars, languageId, pattern, compiled.postFilter, compiled.postFilterParams);
|
|
1473
|
-
matches.push(...fileMatches);
|
|
1474
|
-
}
|
|
1475
|
-
return matches.slice(0, maxResults);
|
|
1476
|
-
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Run a preloaded query definition against a single file.
|
|
1479
|
-
*
|
|
1480
|
-
* Optimized for dispatch runner usage to avoid per-query directory scans.
|
|
1481
|
-
*/
|
|
1482
|
-
async runQueryOnFile(queryDef, filePath, languageId, options = {}, contentOverride) {
|
|
1483
|
-
if (!this.initialized) {
|
|
1484
|
-
const ok = await this.init();
|
|
1485
|
-
if (!ok)
|
|
1486
|
-
return [];
|
|
1487
|
-
}
|
|
1488
|
-
// Compile against the language the FILE is parsed as, never the query's
|
|
1489
|
-
// own `language:` key. A Query is bound to the grammar it compiled
|
|
1490
|
-
// against; run it on a tree from another grammar and tree-sitter returns
|
|
1491
|
-
// zero matches forever — which is what the javascript→typescript rule
|
|
1492
|
-
// merge had been doing (31 queries per JS file, structurally unable to
|
|
1493
|
-
// fire). `getQueryCacheKey` already namespaces by language.
|
|
1494
|
-
const compiled = await this.compileRawQuery(queryDef.id, queryDef.query, queryDef.metavars, languageId, queryDef.post_filter, queryDef.post_filter_params);
|
|
1495
|
-
if (!compiled)
|
|
1496
|
-
return [];
|
|
1497
|
-
const matches = await this.searchFileWithQuery(filePath, compiled.query, compiled.metavars, languageId, queryDef.id, compiled.postFilter, compiled.postFilterParams, contentOverride);
|
|
1498
|
-
const maxResults = options.maxResults ?? 50;
|
|
1499
|
-
return matches.slice(0, maxResults);
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Run a whole rule set against one file in a SINGLE tree walk (#675).
|
|
1503
|
-
*
|
|
1504
|
-
* Calling `runQueryOnFile` per rule re-walks the tree once per rule — ~34
|
|
1505
|
-
* walks per file on a project scan, measured at 3.3× the cost of one
|
|
1506
|
-
* combined query for byte-identical matches. Patterns are concatenated in
|
|
1507
|
-
* rule order and `match.patternIndex` maps back to the owning rule, so
|
|
1508
|
-
* per-rule metavars, predicates, post-filters and caps still apply and
|
|
1509
|
-
* results stay grouped in rule order. Rules that don't compile against this
|
|
1510
|
-
* language are dropped individually (never poisoning the batch), and a
|
|
1511
|
-
* combined query that fails to compile falls back to per-rule execution.
|
|
1512
|
-
*/
|
|
1513
|
-
async runQueriesOnFile(queryDefs, filePath, languageId, options = {}, contentOverride) {
|
|
1514
|
-
if (queryDefs.length === 0)
|
|
1515
|
-
return [];
|
|
1516
|
-
if (!this.initialized) {
|
|
1517
|
-
const ok = await this.init();
|
|
1518
|
-
if (!ok)
|
|
1519
|
-
return [];
|
|
1520
|
-
}
|
|
1521
|
-
const maxResults = options.maxResults ?? 50;
|
|
1522
|
-
const batch = await this.compileQueryBatch(queryDefs, languageId);
|
|
1523
|
-
if (!batch) {
|
|
1524
|
-
// Fallback: one walk per rule, preserving rule order.
|
|
1525
|
-
const results = [];
|
|
1526
|
-
for (const queryDef of queryDefs) {
|
|
1527
|
-
const matches = await this.runQueryOnFile(queryDef, filePath, languageId, options, contentOverride);
|
|
1528
|
-
for (const match of matches)
|
|
1529
|
-
results.push({ queryDef, match });
|
|
1530
|
-
}
|
|
1531
|
-
return results;
|
|
1532
|
-
}
|
|
1533
|
-
const perQuery = new Map();
|
|
1534
|
-
await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
|
|
1535
|
-
try {
|
|
1536
|
-
const rootNode = tree.rootNode;
|
|
1537
|
-
for (const match of batch.query.matches(rootNode)) {
|
|
1538
|
-
const owner = batch.ownerOfPattern[match.patternIndex];
|
|
1539
|
-
if (owner === undefined)
|
|
1540
|
-
continue;
|
|
1541
|
-
const bucket = perQuery.get(owner) ?? [];
|
|
1542
|
-
if (bucket.length >= maxResults)
|
|
1543
|
-
continue;
|
|
1544
|
-
const entry = batch.entries[owner];
|
|
1545
|
-
const captures = {};
|
|
1546
|
-
for (const capture of match.captures) {
|
|
1547
|
-
if (entry.metavars.includes(capture.name)) {
|
|
1548
|
-
captures[capture.name] = capture.node;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
if (!this.evaluatePredicates(batch.query, match))
|
|
1552
|
-
continue;
|
|
1553
|
-
if (entry.postFilter &&
|
|
1554
|
-
!this.applyPostFilter(entry.postFilter, entry.postFilterParams, captures, rootNode)) {
|
|
1555
|
-
continue;
|
|
1556
|
-
}
|
|
1557
|
-
if (match.captures.length === 0)
|
|
1558
|
-
continue;
|
|
1559
|
-
const firstNode = match.captures[0].node;
|
|
1560
|
-
const textCaptures = {};
|
|
1561
|
-
for (const [name, node] of Object.entries(captures)) {
|
|
1562
|
-
textCaptures[name] = node.text;
|
|
1563
|
-
}
|
|
1564
|
-
bucket.push({
|
|
1565
|
-
file: filePath,
|
|
1566
|
-
line: firstNode.startPosition.row + 1,
|
|
1567
|
-
column: firstNode.startPosition.column + 1,
|
|
1568
|
-
matchedText: firstNode.text,
|
|
1569
|
-
nodeType: firstNode.type,
|
|
1570
|
-
captures: textCaptures,
|
|
1571
|
-
});
|
|
1572
|
-
perQuery.set(owner, bucket);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
catch (err) {
|
|
1576
|
-
this.reportWasmAbort(err);
|
|
1577
|
-
this.dbg(`Batched query matching error: ${err}`);
|
|
1578
|
-
}
|
|
1579
|
-
});
|
|
1580
|
-
const results = [];
|
|
1581
|
-
for (let i = 0; i < batch.entries.length; i++) {
|
|
1582
|
-
for (const match of perQuery.get(i) ?? []) {
|
|
1583
|
-
results.push({ queryDef: batch.entries[i].queryDef, match });
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
return results;
|
|
1587
|
-
}
|
|
1588
|
-
/**
|
|
1589
|
-
* Compile `queryDefs` into one multi-pattern Query for `languageId`, with a
|
|
1590
|
-
* pattern-index → rule map. Cached per language + rule-set identity.
|
|
1591
|
-
*/
|
|
1592
|
-
async compileQueryBatch(queryDefs, languageId) {
|
|
1593
|
-
// Key on rule CONTENT, not just ids: the batch stores each queryDef (its
|
|
1594
|
-
// message reaches diagnostics) and the compiled patterns. Rule ids are
|
|
1595
|
-
// stable across edits, so an id-only key kept serving the pre-edit batch
|
|
1596
|
-
// for the process lifetime even after the runner reloaded the rules from
|
|
1597
|
-
// disk on a RuleCache miss (#878). sha256 keeps the key bounded without
|
|
1598
|
-
// #889's 32-bit collision risk.
|
|
1599
|
-
const identity = crypto
|
|
1600
|
-
.createHash("sha256")
|
|
1601
|
-
.update(JSON.stringify(queryDefs))
|
|
1602
|
-
.digest("hex");
|
|
1603
|
-
const cacheKey = this.getQueryCacheKey(`batch:${identity}`, languageId);
|
|
1604
|
-
const cached = this.queryBatchCache.get(cacheKey);
|
|
1605
|
-
if (cached !== undefined) {
|
|
1606
|
-
this.queryBatchCache.delete(cacheKey);
|
|
1607
|
-
this.queryBatchCache.set(cacheKey, cached);
|
|
1608
|
-
return cached;
|
|
1609
|
-
}
|
|
1610
|
-
// A loadLanguage() failure is transient (offline lazy grammar fetch,
|
|
1611
|
-
// transient mid-scan load error) — do NOT cache null for it, or every
|
|
1612
|
-
// later scan pays the ~3.3x per-rule fallback for the process lifetime
|
|
1613
|
-
// (#889). Retry on the next call, bounded so a grammar that never loads
|
|
1614
|
-
// doesn't re-attempt every scan.
|
|
1615
|
-
const language = await this.loadLanguage(languageId);
|
|
1616
|
-
if (!language) {
|
|
1617
|
-
const failures = (this.queryBatchLoadFailures.get(cacheKey) ?? 0) + 1;
|
|
1618
|
-
if (failures >= QUERY_BATCH_MAX_LOAD_FAILURES) {
|
|
1619
|
-
this.dbg(`Batch: grammar for ${languageId} failed to load ${failures} times — caching miss`);
|
|
1620
|
-
this.queryBatchLoadFailures.delete(cacheKey);
|
|
1621
|
-
this.cacheQueryBatch(cacheKey, null);
|
|
1622
|
-
}
|
|
1623
|
-
else {
|
|
1624
|
-
this.queryBatchLoadFailures.set(cacheKey, failures);
|
|
1625
|
-
}
|
|
1626
|
-
return null;
|
|
1627
|
-
}
|
|
1628
|
-
this.queryBatchLoadFailures.delete(cacheKey);
|
|
1629
|
-
const build = async () => {
|
|
1630
|
-
const Query = (await loadWebTreeSitter()).Query;
|
|
1631
|
-
const entries = [];
|
|
1632
|
-
const sources = [];
|
|
1633
|
-
const ownerOfPattern = [];
|
|
1634
|
-
for (const queryDef of queryDefs) {
|
|
1635
|
-
let patternCount;
|
|
1636
|
-
try {
|
|
1637
|
-
// biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
|
|
1638
|
-
const probe = new Query(language, queryDef.query);
|
|
1639
|
-
patternCount = probe.patternCount();
|
|
1640
|
-
probe.delete?.();
|
|
1641
|
-
}
|
|
1642
|
-
catch (err) {
|
|
1643
|
-
if (this.reportWasmAbort(err))
|
|
1644
|
-
return null;
|
|
1645
|
-
// A rule that can't compile against THIS grammar is simply not
|
|
1646
|
-
// applicable here (e.g. a `type_annotation` pattern on javascript).
|
|
1647
|
-
this.dbg(`Batch: skipping ${queryDef.id} for ${languageId}: ${err}`);
|
|
1648
|
-
this.reportQueryCompileFailure(queryDef.id, languageId, err);
|
|
1649
|
-
continue;
|
|
1650
|
-
}
|
|
1651
|
-
const owner = entries.length;
|
|
1652
|
-
entries.push({
|
|
1653
|
-
queryDef,
|
|
1654
|
-
metavars: queryDef.metavars ?? [],
|
|
1655
|
-
postFilter: queryDef.post_filter,
|
|
1656
|
-
postFilterParams: queryDef.post_filter_params,
|
|
1657
|
-
});
|
|
1658
|
-
sources.push(queryDef.query);
|
|
1659
|
-
for (let i = 0; i < patternCount; i++)
|
|
1660
|
-
ownerOfPattern.push(owner);
|
|
1661
|
-
}
|
|
1662
|
-
if (entries.length === 0)
|
|
1663
|
-
return null;
|
|
1664
|
-
try {
|
|
1665
|
-
// biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
|
|
1666
|
-
const query = new Query(language, sources.join("\n"));
|
|
1667
|
-
if (query.patternCount() !== ownerOfPattern.length) {
|
|
1668
|
-
this.dbg(`Batch pattern count mismatch for ${languageId} (${query.patternCount()} vs ${ownerOfPattern.length}) — falling back`);
|
|
1669
|
-
return null;
|
|
1670
|
-
}
|
|
1671
|
-
return { query, entries, ownerOfPattern };
|
|
1672
|
-
}
|
|
1673
|
-
catch (err) {
|
|
1674
|
-
if (this.reportWasmAbort(err))
|
|
1675
|
-
return null;
|
|
1676
|
-
this.dbg(`Batch compile failed for ${languageId}: ${err}`);
|
|
1677
|
-
return null;
|
|
1678
|
-
}
|
|
1679
|
-
};
|
|
1680
|
-
const batch = await build();
|
|
1681
|
-
this.cacheQueryBatch(cacheKey, batch);
|
|
1682
|
-
return batch;
|
|
1683
|
-
}
|
|
1684
|
-
/**
|
|
1685
|
-
* Convert pattern to tree-sitter query
|
|
1686
|
-
* First tries to load from query files, then falls back to inline patterns
|
|
1687
|
-
*/
|
|
1688
|
-
patternToQuery(pattern, languageId) {
|
|
1689
|
-
// Try to find matching query from loaded files
|
|
1690
|
-
const loadedQuery = this.queryLoader.findMatchingQuery(pattern, languageId);
|
|
1691
|
-
if (loadedQuery) {
|
|
1692
|
-
this.dbg(`Using loaded query: ${loadedQuery.id}`);
|
|
1693
|
-
return {
|
|
1694
|
-
query: loadedQuery.query,
|
|
1695
|
-
metavars: loadedQuery.metavars,
|
|
1696
|
-
postFilter: loadedQuery.post_filter,
|
|
1697
|
-
postFilterParams: loadedQuery.post_filter_params,
|
|
1698
|
-
queryDef: loadedQuery,
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
// Fallback to inline patterns
|
|
1702
|
-
return this.getInlinePattern(pattern);
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Inline patterns as fallback when no query file matches
|
|
1706
|
-
*/
|
|
1707
|
-
getInlinePattern(pattern) {
|
|
1708
|
-
// Pattern: async function $NAME($$$PARAMS) { $BODY }
|
|
1709
|
-
if (pattern.includes("async function") && pattern.includes("$NAME")) {
|
|
1710
|
-
return {
|
|
1711
|
-
query: `(function_declaration
|
|
1712
|
-
"async"
|
|
1713
|
-
name: (identifier) @NAME
|
|
1714
|
-
parameters: (formal_parameters) @PARAMS
|
|
1715
|
-
body: (statement_block) @BODY)`,
|
|
1716
|
-
metavars: ["NAME", "PARAMS", "BODY"],
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
// Pattern: console.$METHOD($MSG)
|
|
1720
|
-
if (pattern.includes("console")) {
|
|
1721
|
-
return {
|
|
1722
|
-
query: `(call_expression
|
|
1723
|
-
function: (member_expression
|
|
1724
|
-
object: (identifier) @OBJ (#eq? @OBJ "console")
|
|
1725
|
-
property: (property_identifier) @METHOD)
|
|
1726
|
-
arguments: (arguments) @ARGS)`,
|
|
1727
|
-
metavars: ["OBJ", "METHOD", "ARGS"],
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
// Pattern: function $NAME($$$PARAMS) { $BODY } - match long parameter lists
|
|
1731
|
-
if (pattern.includes("function $NAME") && pattern.includes("PARAMS")) {
|
|
1732
|
-
return {
|
|
1733
|
-
query: `(function_declaration
|
|
1734
|
-
name: (identifier) @NAME
|
|
1735
|
-
parameters: (formal_parameters) @PARAMS
|
|
1736
|
-
body: (statement_block) @BODY)`,
|
|
1737
|
-
metavars: ["NAME", "PARAMS", "BODY"],
|
|
1738
|
-
postFilter: "count_params",
|
|
1739
|
-
postFilterParams: { min_params: 6 },
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
// Pattern: promise chains with .then().catch().then() - 3+ levels
|
|
1743
|
-
if (pattern.includes(".then") && pattern.includes(".catch")) {
|
|
1744
|
-
return {
|
|
1745
|
-
query: `(call_expression
|
|
1746
|
-
function: (member_expression
|
|
1747
|
-
object: (call_expression
|
|
1748
|
-
function: (member_expression
|
|
1749
|
-
object: (call_expression
|
|
1750
|
-
function: (member_expression
|
|
1751
|
-
property: (property_identifier) @M1)
|
|
1752
|
-
arguments: (arguments))
|
|
1753
|
-
property: (property_identifier) @M2)
|
|
1754
|
-
arguments: (arguments))
|
|
1755
|
-
property: (property_identifier) @M3)
|
|
1756
|
-
arguments: (arguments))
|
|
1757
|
-
(#match? @M1 "^(then|catch)$")
|
|
1758
|
-
(#match? @M2 "^(then|catch)$")
|
|
1759
|
-
(#match? @M3 "^(then|catch)$")`,
|
|
1760
|
-
metavars: ["M1", "M2", "M3"],
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
// Fallback: try to create a simple identifier capture
|
|
1764
|
-
const simpleMatch = pattern.match(/\$([A-Z_][A-Z0-9_]*)/);
|
|
1765
|
-
if (simpleMatch) {
|
|
1766
|
-
const name = simpleMatch[1];
|
|
1767
|
-
return {
|
|
1768
|
-
query: `(identifier) @${name}`,
|
|
1769
|
-
metavars: [name],
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
// If we can't convert, return empty to trigger fallback
|
|
1773
|
-
return { query: "", metavars: [] };
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* Inject native tree-sitter predicates into S-expression query
|
|
1777
|
-
* This moves text filtering to WASM for better performance
|
|
1778
|
-
*/
|
|
1779
|
-
/** Generate cache key for compiled query */
|
|
1780
|
-
getQueryCacheKey(pattern, languageId) {
|
|
1781
|
-
// Full pattern text as the key. The previous 32-bit hash could collide
|
|
1782
|
-
// across rules and permanently poison a cache slot with the wrong
|
|
1783
|
-
// compiled query (or a null meant for a different rule set) (#889).
|
|
1784
|
-
return `${languageId}:${pattern}`;
|
|
1785
|
-
}
|
|
1786
|
-
/** Compile a pattern into a tree-sitter Query with caching */
|
|
1787
|
-
async compileQuery(pattern, languageId) {
|
|
1788
|
-
const cacheKey = this.getQueryCacheKey(pattern, languageId);
|
|
1789
|
-
// Check cache first
|
|
1790
|
-
if (this.queryCache.has(cacheKey)) {
|
|
1791
|
-
this.dbg(`Query cache hit: ${cacheKey}`);
|
|
1792
|
-
const cached = this.queryCache.get(cacheKey);
|
|
1793
|
-
this.queryCache.delete(cacheKey);
|
|
1794
|
-
this.queryCache.set(cacheKey, cached);
|
|
1795
|
-
return cached;
|
|
1796
|
-
}
|
|
1797
|
-
const language = await this.loadLanguage(languageId);
|
|
1798
|
-
if (!language) {
|
|
1799
|
-
this.dbg(`Could not load language ${languageId}`);
|
|
1800
|
-
return null;
|
|
1801
|
-
}
|
|
1802
|
-
const { query: queryStr, metavars, postFilter, postFilterParams, } = this.patternToQuery(pattern, languageId);
|
|
1803
|
-
this.dbg(`Query string: ${queryStr.slice(0, 100)}...`);
|
|
1804
|
-
try {
|
|
1805
|
-
// biome-ignore lint/suspicious/noExplicitAny: Query constructor
|
|
1806
|
-
const Query = (await loadWebTreeSitter()).Query;
|
|
1807
|
-
// biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
|
|
1808
|
-
const query = new Query(language, queryStr);
|
|
1809
|
-
this.dbg(`Query compiled with ${query.patternCount()} patterns`);
|
|
1810
|
-
const result = { query, metavars, postFilter, postFilterParams };
|
|
1811
|
-
// Cache the compiled query
|
|
1812
|
-
this.cacheQuery(cacheKey, result);
|
|
1813
|
-
return result;
|
|
1814
|
-
}
|
|
1815
|
-
catch (err) {
|
|
1816
|
-
this.reportWasmAbort(err);
|
|
1817
|
-
this.dbg(`Query compilation failed: ${err}`);
|
|
1818
|
-
return null;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
/** Compile a raw tree-sitter query string with caching */
|
|
1822
|
-
async compileRawQuery(queryId, queryStr, metavars, languageId, postFilter, postFilterParams) {
|
|
1823
|
-
const cacheKey = this.getQueryCacheKey(`raw:${queryId}:${queryStr}`, languageId);
|
|
1824
|
-
if (this.queryCache.has(cacheKey)) {
|
|
1825
|
-
const cached = this.queryCache.get(cacheKey);
|
|
1826
|
-
this.queryCache.delete(cacheKey);
|
|
1827
|
-
this.queryCache.set(cacheKey, cached);
|
|
1828
|
-
return cached;
|
|
1829
|
-
}
|
|
1830
|
-
const language = await this.loadLanguage(languageId);
|
|
1831
|
-
if (!language)
|
|
1832
|
-
return null;
|
|
1833
|
-
try {
|
|
1834
|
-
// biome-ignore lint/suspicious/noExplicitAny: Query constructor from web-tree-sitter
|
|
1835
|
-
const Query = (await loadWebTreeSitter()).Query;
|
|
1836
|
-
// biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
|
|
1837
|
-
const query = new Query(language, queryStr);
|
|
1838
|
-
const result = { query, metavars, postFilter, postFilterParams };
|
|
1839
|
-
this.cacheQuery(cacheKey, result);
|
|
1840
|
-
return result;
|
|
1841
|
-
}
|
|
1842
|
-
catch (err) {
|
|
1843
|
-
this.reportWasmAbort(err);
|
|
1844
|
-
this.dbg(`Raw query compilation failed (${queryId}): ${err}`);
|
|
1845
|
-
this.reportQueryCompileFailure(queryId, languageId, err);
|
|
1846
|
-
return null;
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
reportedCompileFailures = new Set();
|
|
1850
|
-
/** Warn once per rule+grammar pair whose query fails to compile — a silently-dead rule needs a trail. */
|
|
1851
|
-
reportQueryCompileFailure(ruleId, languageId, err) {
|
|
1852
|
-
// Keyed by rule AND language: a rule can be dispatched against more than one
|
|
1853
|
-
// grammar (`queriesForLanguage` hands the typescript set to tsx too), and
|
|
1854
|
-
// failing on one must not mute the report for the others.
|
|
1855
|
-
const key = `${ruleId}:${languageId}`;
|
|
1856
|
-
if (this.reportedCompileFailures.has(key))
|
|
1857
|
-
return;
|
|
1858
|
-
this.reportedCompileFailures.add(key);
|
|
1859
|
-
logTreeSitterDiagnostic({
|
|
1860
|
-
subsystem: "tree-sitter-client",
|
|
1861
|
-
languageId,
|
|
1862
|
-
message: `tree-sitter rule '${ruleId}' failed to compile against '${languageId}' — ` +
|
|
1863
|
-
`matches for this rule are silently dropped rather than reported. ` +
|
|
1864
|
-
`Fix the query in the rule definition to re-enable it. (${err})`,
|
|
1865
|
-
metadata: { ruleId },
|
|
1866
|
-
});
|
|
1867
|
-
}
|
|
1868
|
-
hasChildToken(node, token) {
|
|
1869
|
-
return node.children?.some((child) => child.type === token || child.text === token);
|
|
1870
|
-
}
|
|
1871
|
-
/**
|
|
1872
|
-
* Collect names introduced by a parameter binding pattern.
|
|
1873
|
-
*
|
|
1874
|
-
* Binding-aware: only identifiers in a *binding* position are counted.
|
|
1875
|
-
* Two node shapes hold reference expressions rather than bindings and
|
|
1876
|
-
* must not be descended into:
|
|
1877
|
-
* - `assignment_pattern` / `object_assignment_pattern` (`pattern = default`,
|
|
1878
|
-
* e.g. `[a = b]` or `{a = b}`) — the `value`/`right` side is a default
|
|
1879
|
-
* *expression*, not a new name (`b` in `{a = b}` is a reference).
|
|
1880
|
-
* - `computed_property_name` (`[expr]:` in a destructuring pattern,
|
|
1881
|
-
* e.g. `{[key]: a}`) — `expr` is a reference, not a binding.
|
|
1882
|
-
* Nested binding positions (destructured sub-patterns) are still walked
|
|
1883
|
-
* so renamed/duplicate collisions inside them are found.
|
|
1884
|
-
*/
|
|
1885
|
-
bindingNames(node) {
|
|
1886
|
-
const names = new Set();
|
|
1887
|
-
if (!node)
|
|
1888
|
-
return names;
|
|
1889
|
-
const stack = [node];
|
|
1890
|
-
while (stack.length > 0) {
|
|
1891
|
-
const current = stack.pop();
|
|
1892
|
-
if (current.type === "identifier")
|
|
1893
|
-
names.add(current.text);
|
|
1894
|
-
else if (current.type === "shorthand_property_identifier_pattern") {
|
|
1895
|
-
names.add(current.text);
|
|
1896
|
-
}
|
|
1897
|
-
if (current.type === "property_identifier" ||
|
|
1898
|
-
current.type === "type_identifier" ||
|
|
1899
|
-
current.type === "computed_property_name")
|
|
1900
|
-
continue;
|
|
1901
|
-
if (current.type === "assignment_pattern" ||
|
|
1902
|
-
current.type === "object_assignment_pattern") {
|
|
1903
|
-
const left = current.childForFieldName?.("left");
|
|
1904
|
-
if (left)
|
|
1905
|
-
stack.push(left);
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
stack.push(...(current.children ?? []));
|
|
1909
|
-
}
|
|
1910
|
-
return names;
|
|
1911
|
-
}
|
|
1912
|
-
containsYieldInFunctionBody(node, root = node) {
|
|
1913
|
-
for (const child of node.children ?? []) {
|
|
1914
|
-
if (child.type === "yield")
|
|
1915
|
-
return true;
|
|
1916
|
-
if (child !== root &&
|
|
1917
|
-
["function_definition", "class_definition", "lambda"].includes(child.type)) {
|
|
1918
|
-
continue;
|
|
1919
|
-
}
|
|
1920
|
-
if (this.containsYieldInFunctionBody(child, root))
|
|
1921
|
-
return true;
|
|
1922
|
-
}
|
|
1923
|
-
return false;
|
|
1924
|
-
}
|
|
1925
|
-
/**
|
|
1926
|
-
* SQLAlchemy sessions are conventionally bound to one of a handful of
|
|
1927
|
-
* receiver names. Structural provenance (`python-provenance.ts`) proves the
|
|
1928
|
-
* annotated case; this name check keeps the far more common unannotated
|
|
1929
|
-
* `session.execute(stmt)` quiet, as it has been since the exemption was
|
|
1930
|
-
* added. Removing it regresses every codebase that never annotates.
|
|
1931
|
-
*/
|
|
1932
|
-
isLikelySqlAlchemyReceiver(text) {
|
|
1933
|
-
const tail = text.split(".").pop() ?? text;
|
|
1934
|
-
return PYTHON_SQLALCHEMY_RECEIVER_NAMES.has(tail.toLowerCase());
|
|
1935
|
-
}
|
|
1936
|
-
/**
|
|
1937
|
-
* The body statements of a `switch_case`, in order. A switch_case's named
|
|
1938
|
-
* children are `[value, ...statements]` (its statements are direct children,
|
|
1939
|
-
* not a wrapping statement_block), so drop the leading `case <value>` and any
|
|
1940
|
-
* comments.
|
|
1941
|
-
*/
|
|
1942
|
-
switchCaseBodyStatements(caseNode) {
|
|
1943
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST child iteration
|
|
1944
|
-
const named = (caseNode.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
|
|
1945
|
-
// First named child is the case's value expression (`case <value>`).
|
|
1946
|
-
return named.slice(1);
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* Whether a case body carries an intentional fallthrough marker comment
|
|
1950
|
-
* (`// fallthrough`, `// falls through`, …). Only a *trailing* marker is
|
|
1951
|
-
* honored: a comment attached to the case node after its body statements
|
|
1952
|
-
* (TypeScript), the case's next sibling in the switch body (JavaScript), or
|
|
1953
|
-
* one inside the trailing body statement's block. A comment in a nested
|
|
1954
|
-
* function or an earlier statement is not a marker for this case and must
|
|
1955
|
-
* not suppress a genuine fall-through. Only comment nodes are checked, never
|
|
1956
|
-
* the case value or statement text.
|
|
1957
|
-
*/
|
|
1958
|
-
hasFallthroughMarker(caseNode) {
|
|
1959
|
-
const comments = [];
|
|
1960
|
-
const body = this.switchCaseBodyStatements(caseNode);
|
|
1961
|
-
const last = body[body.length - 1];
|
|
1962
|
-
const lastEnd = last?.endIndex ?? caseNode.startIndex;
|
|
1963
|
-
// TypeScript attaches a trailing `// fallthrough` as a direct child of the
|
|
1964
|
-
// case node, after the body statements.
|
|
1965
|
-
for (const c of caseNode.children ?? []) {
|
|
1966
|
-
if (c.type.includes("comment") && c.startIndex >= lastEnd) {
|
|
1967
|
-
comments.push(c);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
// JavaScript attaches it to the switch body as the case's next sibling.
|
|
1971
|
-
const siblings = (caseNode.parent?.children ?? []).filter((c) => c.isNamed);
|
|
1972
|
-
const index = siblings.findIndex((c) => c.startIndex === caseNode.startIndex);
|
|
1973
|
-
const next = index >= 0 ? siblings[index + 1] : undefined;
|
|
1974
|
-
if (next?.type.includes("comment"))
|
|
1975
|
-
comments.push(next);
|
|
1976
|
-
// A marker inside the trailing body statement's block (e.g. `{ work();
|
|
1977
|
-
// /* fallthrough */ }`). Only the trailing statement is walked, and nested
|
|
1978
|
-
// functions are not descended into, so a comment in a nested function or
|
|
1979
|
-
// an earlier statement can't suppress a genuine fall-through.
|
|
1980
|
-
if (last) {
|
|
1981
|
-
const NESTED_FN = new Set([
|
|
1982
|
-
"function_declaration",
|
|
1983
|
-
"function_expression",
|
|
1984
|
-
"arrow_function",
|
|
1985
|
-
"method_definition",
|
|
1986
|
-
"generator_function",
|
|
1987
|
-
"generator_function_declaration",
|
|
1988
|
-
"class_declaration",
|
|
1989
|
-
]);
|
|
1990
|
-
const stack = [last];
|
|
1991
|
-
for (let visited = 0; stack.length > 0 && visited < 500; visited++) {
|
|
1992
|
-
const node = stack.pop();
|
|
1993
|
-
if (!node)
|
|
1994
|
-
break;
|
|
1995
|
-
if (node.type.includes("comment"))
|
|
1996
|
-
comments.push(node);
|
|
1997
|
-
if (!NESTED_FN.has(node.type)) {
|
|
1998
|
-
stack.push(...(node.children ?? []));
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
return comments.some((c) => /falls?\s?through/i.test(c.text));
|
|
2003
|
-
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Whether a statement terminates control flow (does not fall through to the
|
|
2006
|
-
* next statement). Handles terminators, trailing blocks, try/catch/finally,
|
|
2007
|
-
* and exhaustive if/else. Fail-safe: any unrecognized shape returns false
|
|
2008
|
-
* (falls through), so a blocking rule never under-reports. Depth-bounded so a
|
|
2009
|
-
* pathological nesting cannot blow the stack.
|
|
2010
|
-
*/
|
|
2011
|
-
statementTerminates(node, depth = 0) {
|
|
2012
|
-
if (depth > 20)
|
|
2013
|
-
return false;
|
|
2014
|
-
const t = node.type;
|
|
2015
|
-
const TERMINATORS = new Set([
|
|
2016
|
-
"break_statement",
|
|
2017
|
-
"return_statement",
|
|
2018
|
-
"throw_statement",
|
|
2019
|
-
"continue_statement",
|
|
2020
|
-
]);
|
|
2021
|
-
if (TERMINATORS.has(t))
|
|
2022
|
-
return true;
|
|
2023
|
-
// Wrapper nodes whose trailing statement decides termination: a block, an
|
|
2024
|
-
// `else` clause, a `catch` clause, or a `finally` clause.
|
|
2025
|
-
if (t === "statement_block" ||
|
|
2026
|
-
t === "else_clause" ||
|
|
2027
|
-
t === "catch_clause" ||
|
|
2028
|
-
t === "finally_clause") {
|
|
2029
|
-
const inner = (node.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
|
|
2030
|
-
const last = inner[inner.length - 1];
|
|
2031
|
-
if (!last)
|
|
2032
|
-
return false; // empty wrapper falls through
|
|
2033
|
-
return this.statementTerminates(last, depth + 1);
|
|
2034
|
-
}
|
|
2035
|
-
if (t === "try_statement") {
|
|
2036
|
-
// The try body is the first statement_block child. A returning/throwing
|
|
2037
|
-
// try completes after finally, so only a catch that handles a thrown try
|
|
2038
|
-
// and falls through can still reach the end. A try body that completes
|
|
2039
|
-
// normally reaches the end unless a finally terminates it.
|
|
2040
|
-
const tryBody = (node.children ?? []).find((c) => c.type === "statement_block");
|
|
2041
|
-
const tryTerminates = tryBody
|
|
2042
|
-
? this.statementTerminates(tryBody, depth + 1)
|
|
2043
|
-
: false;
|
|
2044
|
-
const catches = (node.children ?? []).filter((c) => c.type === "catch_clause");
|
|
2045
|
-
const fin = (node.children ?? []).find((c) => c.type === "finally_clause");
|
|
2046
|
-
if (tryTerminates) {
|
|
2047
|
-
return catches.every((c) => this.statementTerminates(c, depth + 1));
|
|
2048
|
-
}
|
|
2049
|
-
return !!fin && this.statementTerminates(fin, depth + 1);
|
|
2050
|
-
}
|
|
2051
|
-
if (t === "if_statement") {
|
|
2052
|
-
// An if only terminates when it has an else and both branches do.
|
|
2053
|
-
const named = (node.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
|
|
2054
|
-
// named = [condition, consequence, (else_clause)]
|
|
2055
|
-
const consequence = named[1];
|
|
2056
|
-
const alternative = named[2];
|
|
2057
|
-
if (!consequence || !alternative)
|
|
2058
|
-
return false;
|
|
2059
|
-
return (this.statementTerminates(consequence, depth + 1) &&
|
|
2060
|
-
this.statementTerminates(alternative, depth + 1));
|
|
2061
|
-
}
|
|
2062
|
-
return false;
|
|
2063
|
-
}
|
|
2064
|
-
/**
|
|
2065
|
-
* Whether a `switch_case`'s next sibling is another label — `case "a": case
|
|
2066
|
-
* "b": handle()` groups two labels onto one body, which is idiomatic, not a
|
|
2067
|
-
* dead case.
|
|
2068
|
-
*/
|
|
2069
|
-
nextSiblingIsSwitchLabel(caseNode) {
|
|
2070
|
-
const siblings = (caseNode.parent?.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
|
|
2071
|
-
// Match by source offset: web-tree-sitter hands out fresh node wrappers per
|
|
2072
|
-
// access, so identity comparison against `caseNode` is not reliable.
|
|
2073
|
-
const index = siblings.findIndex((c) => c.startIndex === caseNode.startIndex);
|
|
2074
|
-
const next = index >= 0 ? siblings[index + 1] : undefined;
|
|
2075
|
-
return next?.type === "switch_case" || next?.type === "switch_default";
|
|
2076
|
-
}
|
|
2077
|
-
/**
|
|
2078
|
-
* Whether a loop body contains a statement that can terminate the loop:
|
|
2079
|
-
* `return`/`throw` anywhere (they unwind past the loop), or a `break` that is
|
|
2080
|
-
* not swallowed by a nested loop/switch. Does not descend into nested
|
|
2081
|
-
* functions, whose `return` exits the function rather than the loop.
|
|
2082
|
-
*/
|
|
2083
|
-
bodyHasLoopExit(node, insideNestedLoop) {
|
|
2084
|
-
const NESTS_BREAK = new Set([
|
|
2085
|
-
"for_statement",
|
|
2086
|
-
"for_in_statement",
|
|
2087
|
-
"while_statement",
|
|
2088
|
-
"do_statement",
|
|
2089
|
-
"switch_statement",
|
|
2090
|
-
]);
|
|
2091
|
-
const NESTED_FN = new Set([
|
|
2092
|
-
"function_declaration",
|
|
2093
|
-
"function_expression",
|
|
2094
|
-
"arrow_function",
|
|
2095
|
-
"method_definition",
|
|
2096
|
-
"generator_function",
|
|
2097
|
-
"generator_function_declaration",
|
|
2098
|
-
"class_declaration",
|
|
2099
|
-
]);
|
|
2100
|
-
for (const child of node.children ?? []) {
|
|
2101
|
-
const t = child.type;
|
|
2102
|
-
if (NESTED_FN.has(t))
|
|
2103
|
-
continue;
|
|
2104
|
-
if (t === "return_statement" || t === "throw_statement")
|
|
2105
|
-
return true;
|
|
2106
|
-
// A labeled `break outer;` is not swallowed by the nested loop it sits
|
|
2107
|
-
// in. Which label it targets isn't resolved here — counting any labeled
|
|
2108
|
-
// break as an exit errs toward not flagging, the safe direction for a
|
|
2109
|
-
// blocking rule.
|
|
2110
|
-
if (t === "break_statement") {
|
|
2111
|
-
const labeled = (child.children ?? []).some((c) => c.isNamed);
|
|
2112
|
-
if (!insideNestedLoop || labeled)
|
|
2113
|
-
return true;
|
|
2114
|
-
}
|
|
2115
|
-
if (this.bodyHasLoopExit(child, insideNestedLoop || NESTS_BREAK.has(t))) {
|
|
2116
|
-
return true;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
return false;
|
|
2120
|
-
}
|
|
2121
|
-
/**
|
|
2122
|
-
* Name of the binding a node's value flows into: the declared name of an
|
|
2123
|
-
* enclosing `variable_declarator`, or the left-hand side of an enclosing
|
|
2124
|
-
* `assignment_expression`. Returns "" if the node is not part of a binding
|
|
2125
|
-
* (the walk stops at function/block/program boundaries so it never reaches
|
|
2126
|
-
* out to an unrelated outer binding).
|
|
2127
|
-
*/
|
|
2128
|
-
enclosingBindingName(node) {
|
|
2129
|
-
let cur = node?.parent;
|
|
2130
|
-
for (let depth = 0; cur && depth < 12; depth++) {
|
|
2131
|
-
const t = cur.type;
|
|
2132
|
-
if (t === "variable_declarator") {
|
|
2133
|
-
const name = cur.children?.find((c) => c.isNamed);
|
|
2134
|
-
return name?.text ?? "";
|
|
2135
|
-
}
|
|
2136
|
-
if (t === "assignment_expression") {
|
|
2137
|
-
return cur.children?.[0]?.text ?? "";
|
|
2138
|
-
}
|
|
2139
|
-
if (t === "statement_block" ||
|
|
2140
|
-
t === "program" ||
|
|
2141
|
-
t === "function_declaration" ||
|
|
2142
|
-
t === "function_expression" ||
|
|
2143
|
-
t === "arrow_function" ||
|
|
2144
|
-
t === "method_definition") {
|
|
2145
|
-
return "";
|
|
2146
|
-
}
|
|
2147
|
-
cur = cur.parent;
|
|
2148
|
-
}
|
|
2149
|
-
return "";
|
|
2150
|
-
}
|
|
2151
|
-
/**
|
|
2152
|
-
* Resolves `name` (as used in the *same file*) to a provably fixed URL:
|
|
2153
|
-
* a `const` declarator whose initializer is a string literal, or a
|
|
2154
|
-
* template literal with no `${...}` substitutions.
|
|
2155
|
-
*
|
|
2156
|
-
* This is deliberately conservative — naming convention (e.g.
|
|
2157
|
-
* SCREAMING_SNAKE_CASE) proves nothing about provenance, so it is never
|
|
2158
|
-
* consulted here. Any of the following makes resolution fail (and the
|
|
2159
|
-
* caller must then treat the identifier as potentially tainted):
|
|
2160
|
-
* - no declarator found for `name` in this file;
|
|
2161
|
-
* - more than one declarator for `name` (ambiguous/shadowed — refuse
|
|
2162
|
-
* rather than guess which one applies at the use site);
|
|
2163
|
-
* - declared with `let`/`var` (not `const`);
|
|
2164
|
-
* - the identifier is reassigned anywhere in the file
|
|
2165
|
-
* (`name = ...`), even if the declaration itself is `const`-like in
|
|
2166
|
-
* spirit — this also catches destructuring/compound-assignment
|
|
2167
|
-
* edge cases conservatively since we only special-case a clean
|
|
2168
|
-
* assignment_expression;
|
|
2169
|
-
* - the initializer is not a plain string/no-substitution template
|
|
2170
|
-
* literal (e.g. `process.env.X`, a function call, a member
|
|
2171
|
-
* expression, another identifier).
|
|
2172
|
-
*/
|
|
2173
|
-
resolvesToFileLiteralConst(name, rootNode) {
|
|
2174
|
-
const valueNode = this.resolveFileConstValueNode(name, rootNode);
|
|
2175
|
-
if (!valueNode)
|
|
2176
|
-
return false;
|
|
2177
|
-
return this.isFixedUrlLiteralExpr(valueNode);
|
|
2178
|
-
}
|
|
2179
|
-
/**
|
|
2180
|
-
* Resolves `name` to the initializer value node of its *single, clean*
|
|
2181
|
-
* file-local `const` declarator, or `null` when resolution must be refused.
|
|
2182
|
-
*
|
|
2183
|
-
* Refusal (returns `null`) on any of: no declarator; more than one
|
|
2184
|
-
* declarator (shadowed/ambiguous — don't guess); a `let`/`var` binding for
|
|
2185
|
-
* the same name anywhere; or a reassignment (`name = ...`) anywhere in the
|
|
2186
|
-
* file. This is the shared, provenance-safe gate used by every "provably
|
|
2187
|
-
* fixed value" check; callers inspect the returned value node themselves.
|
|
2188
|
-
*/
|
|
2189
|
-
resolveFileConstValueNode(name, rootNode) {
|
|
2190
|
-
const constDeclarators = [];
|
|
2191
|
-
let hasNonConstBinding = false;
|
|
2192
|
-
let hasReassignment = false;
|
|
2193
|
-
const stack = [rootNode];
|
|
2194
|
-
while (stack.length > 0) {
|
|
2195
|
-
const node = stack.pop();
|
|
2196
|
-
if (!node)
|
|
2197
|
-
continue;
|
|
2198
|
-
if (node.type === "variable_declarator") {
|
|
2199
|
-
const nameNode = node.childForFieldName?.("name");
|
|
2200
|
-
if (nameNode?.type === "identifier" && nameNode.text === name) {
|
|
2201
|
-
const decl = node.parent;
|
|
2202
|
-
const isConst = decl?.type === "lexical_declaration" &&
|
|
2203
|
-
(decl.children ?? []).some((c) => c.type === "const");
|
|
2204
|
-
if (isConst) {
|
|
2205
|
-
constDeclarators.push(node);
|
|
2206
|
-
}
|
|
2207
|
-
else {
|
|
2208
|
-
hasNonConstBinding = true;
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
else if (node.type === "assignment_expression" ||
|
|
2213
|
-
node.type === "augmented_assignment_expression") {
|
|
2214
|
-
const left = node.childForFieldName?.("left");
|
|
2215
|
-
if (left?.type === "identifier" && left.text === name) {
|
|
2216
|
-
hasReassignment = true;
|
|
2217
|
-
}
|
|
2218
|
-
else if ((left?.type === "member_expression" ||
|
|
2219
|
-
left?.type === "subscript_expression") &&
|
|
2220
|
-
left.childForFieldName?.("object")?.type === "identifier" &&
|
|
2221
|
-
left.childForFieldName?.("object")?.text === name) {
|
|
2222
|
-
// Property/subscript write to the bound receiver
|
|
2223
|
-
// (`name.<prop> = …` / `name[…] = …`). Any origin/host/path/
|
|
2224
|
-
// protocol/port/href mutation re-taints the destination after
|
|
2225
|
-
// construction, so fail closed and treat it as a reassignment
|
|
2226
|
-
// (#1008). The ONE safe exception is a query-string-only write
|
|
2227
|
-
// (`name.search = …`), which never alters the origin;
|
|
2228
|
-
// `name.searchParams.<method>(…)` is a call_expression (not an
|
|
2229
|
-
// assignment) and so never reaches this branch, staying exempt.
|
|
2230
|
-
const searchOnly = left.type === "member_expression" &&
|
|
2231
|
-
left.childForFieldName?.("property")?.text === "search";
|
|
2232
|
-
if (!searchOnly) {
|
|
2233
|
-
hasReassignment = true;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
for (const child of node.children ?? [])
|
|
2238
|
-
stack.push(child);
|
|
2239
|
-
}
|
|
2240
|
-
// Ambiguous (shadowed/duplicated), reassigned anywhere, or backed by a
|
|
2241
|
-
// non-const binding somewhere in the file: refuse to resolve.
|
|
2242
|
-
if (hasNonConstBinding ||
|
|
2243
|
-
hasReassignment ||
|
|
2244
|
-
constDeclarators.length !== 1) {
|
|
2245
|
-
return null;
|
|
2246
|
-
}
|
|
2247
|
-
return constDeclarators[0].childForFieldName?.("value") ?? null;
|
|
2248
|
-
}
|
|
2249
|
-
/**
|
|
2250
|
-
* True when `node` is a self-contained fixed URL string: a plain string
|
|
2251
|
-
* literal, or a template literal with no `${...}` substitutions.
|
|
2252
|
-
*/
|
|
2253
|
-
isFixedUrlLiteralExpr(node) {
|
|
2254
|
-
if (node.type === "string")
|
|
2255
|
-
return true;
|
|
2256
|
-
if (node.type === "template_string") {
|
|
2257
|
-
return !(node.children ?? []).some((c) => c.type === "template_substitution");
|
|
2258
|
-
}
|
|
2259
|
-
return false;
|
|
2260
|
-
}
|
|
2261
|
-
/**
|
|
2262
|
-
* True when `name` is a binding introduced by an `import` in this file
|
|
2263
|
-
* (named/aliased/default/namespace). An import binding is immutable and its
|
|
2264
|
-
* value is fixed at module-load time from source — it is never request- or
|
|
2265
|
-
* attacker-scoped, so an imported base URL is treated as fixed. (Limitation:
|
|
2266
|
-
* we cannot see the exporting module, so an imported value that is itself
|
|
2267
|
-
* `process.env.X` in another file is not distinguished — an accepted, bounded
|
|
2268
|
-
* gap; direct env/param/request taint at the sink still fires.)
|
|
2269
|
-
*/
|
|
2270
|
-
isImportedBinding(name, rootNode) {
|
|
2271
|
-
return this.importedAs(name, rootNode) !== null;
|
|
2272
|
-
}
|
|
2273
|
-
/**
|
|
2274
|
-
* If `name` is an import binding, returns the *imported* name (e.g. `URL`
|
|
2275
|
-
* for `import { URL as NodeURL }` → `importedAs("NodeURL") === "URL"`, and
|
|
2276
|
-
* `importedAs("URL") === "URL"`). Returns `null` when `name` is not imported.
|
|
2277
|
-
*/
|
|
2278
|
-
importedAs(name, rootNode) {
|
|
2279
|
-
const stack = [rootNode];
|
|
2280
|
-
while (stack.length > 0) {
|
|
2281
|
-
const node = stack.pop();
|
|
2282
|
-
if (!node)
|
|
2283
|
-
continue;
|
|
2284
|
-
if (node.type === "import_specifier") {
|
|
2285
|
-
const nameNode = node.childForFieldName?.("name");
|
|
2286
|
-
const aliasNode = node.childForFieldName?.("alias");
|
|
2287
|
-
const local = (aliasNode ?? nameNode)?.text;
|
|
2288
|
-
if (local === name)
|
|
2289
|
-
return nameNode?.text ?? null;
|
|
2290
|
-
}
|
|
2291
|
-
else if (node.type === "namespace_import" ||
|
|
2292
|
-
node.type === "import_clause") {
|
|
2293
|
-
// `import X from …` / `import * as X from …` — default/namespace
|
|
2294
|
-
// local binding is a direct identifier child.
|
|
2295
|
-
for (const c of node.children ?? []) {
|
|
2296
|
-
if (c.type === "identifier" && c.text === name)
|
|
2297
|
-
return name;
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
for (const child of node.children ?? [])
|
|
2301
|
-
stack.push(child);
|
|
2302
|
-
}
|
|
2303
|
-
return null;
|
|
2304
|
-
}
|
|
2305
|
-
/**
|
|
2306
|
-
* True when `base` (the second argument of `new URL(path, base)`) is a
|
|
2307
|
-
* provably fixed origin: a literal/substitution-free template, an identifier
|
|
2308
|
-
* resolving to a file-local literal `const` or an import binding, or a
|
|
2309
|
-
* template whose every `${…}` substitution is such an identifier. Anything
|
|
2310
|
-
* else (function params, `process.env.X`, member expressions, calls) fails.
|
|
2311
|
-
*/
|
|
2312
|
-
isFixedUrlBaseExpr(base, rootNode) {
|
|
2313
|
-
if (base.type === "string")
|
|
2314
|
-
return true;
|
|
2315
|
-
if (base.type === "identifier") {
|
|
2316
|
-
return this.isFixedBaseIdentifier(base, rootNode);
|
|
2317
|
-
}
|
|
2318
|
-
if (base.type === "template_string") {
|
|
2319
|
-
for (const child of base.children ?? []) {
|
|
2320
|
-
if (child.type !== "template_substitution")
|
|
2321
|
-
continue;
|
|
2322
|
-
const inner = (child.children ?? []).find((c) => c.isNamed);
|
|
2323
|
-
if (!inner ||
|
|
2324
|
-
inner.type !== "identifier" ||
|
|
2325
|
-
!this.isFixedBaseIdentifier(inner, rootNode)) {
|
|
2326
|
-
return false;
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
return true;
|
|
2330
|
-
}
|
|
2331
|
-
return false;
|
|
2332
|
-
}
|
|
2333
|
-
/**
|
|
2334
|
-
* True when the identifier `ident` (used as, or inside, a `new URL` base) is
|
|
2335
|
-
* a provably fixed origin AT ITS USE SITE. A file-local literal `const` is
|
|
2336
|
-
* only trusted when no nearer binding shadows it: `resolveFileConstValueNode`
|
|
2337
|
-
* already fails closed on any `let`/`var` of the same name and on multiple
|
|
2338
|
-
* `const` declarators, but function/method PARAMETERS are not variable
|
|
2339
|
-
* declarators and so slip past that gate — a request-tainted parameter base
|
|
2340
|
-
* would otherwise be exempted merely because an unrelated same-named
|
|
2341
|
-
* module-level `const` literal exists (#1008). So we additionally refuse the
|
|
2342
|
-
* file-const path when an enclosing function on the path from the use site to
|
|
2343
|
-
* the module root binds a parameter of the same name. Imported bindings stay
|
|
2344
|
-
* trusted unconditionally (imported-base-as-fixed is sound; see #1000).
|
|
2345
|
-
*/
|
|
2346
|
-
isFixedBaseIdentifier(ident, rootNode) {
|
|
2347
|
-
const name = ident.text;
|
|
2348
|
-
if (!this.isShadowedByEnclosingParam(ident, name) &&
|
|
2349
|
-
this.resolvesToFileLiteralConst(name, rootNode)) {
|
|
2350
|
-
return true;
|
|
2351
|
-
}
|
|
2352
|
-
return this.isImportedBinding(name, rootNode);
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* True when some function/method/arrow on the ancestor chain of `node` (up to
|
|
2356
|
-
* the module root) binds a PARAMETER named `name` — i.e. `name` at `node`'s
|
|
2357
|
-
* location resolves to a parameter, not to an outer `const`. Only binding
|
|
2358
|
-
* positions are inspected (a parameter's `pattern`, including destructured
|
|
2359
|
-
* bindings); default-value expressions (`= expr`) are uses, not bindings, and
|
|
2360
|
-
* are skipped so an outer const referenced in a default is not mistaken for a
|
|
2361
|
-
* shadow. Fail-closed bias: unknown parameter shapes that surface a matching
|
|
2362
|
-
* identifier in a binding position are treated as a shadow.
|
|
2363
|
-
*/
|
|
2364
|
-
isShadowedByEnclosingParam(node, name) {
|
|
2365
|
-
const FUNCTION_TYPES = new Set([
|
|
2366
|
-
"function_declaration",
|
|
2367
|
-
"function_expression",
|
|
2368
|
-
"generator_function",
|
|
2369
|
-
"generator_function_declaration",
|
|
2370
|
-
"arrow_function",
|
|
2371
|
-
"method_definition",
|
|
2372
|
-
]);
|
|
2373
|
-
let current = node.parent;
|
|
2374
|
-
while (current) {
|
|
2375
|
-
if (FUNCTION_TYPES.has(current.type)) {
|
|
2376
|
-
// Bare-identifier arrow parameter: `name => …`.
|
|
2377
|
-
const bare = current.childForFieldName?.("parameter");
|
|
2378
|
-
if (bare?.type === "identifier" && bare.text === name)
|
|
2379
|
-
return true;
|
|
2380
|
-
const params = current.childForFieldName?.("parameters");
|
|
2381
|
-
if (params && this.paramsBindName(params, name))
|
|
2382
|
-
return true;
|
|
2383
|
-
}
|
|
2384
|
-
current = current.parent;
|
|
2385
|
-
}
|
|
2386
|
-
return false;
|
|
2387
|
-
}
|
|
2388
|
-
/** True when a `formal_parameters` node binds `name` in any binding position. */
|
|
2389
|
-
paramsBindName(params, name) {
|
|
2390
|
-
for (const param of params.children ?? []) {
|
|
2391
|
-
if (!param.isNamed)
|
|
2392
|
-
continue;
|
|
2393
|
-
// required_parameter / optional_parameter carry the binding in `pattern`
|
|
2394
|
-
// and the default (a use, not a binding) in `value`.
|
|
2395
|
-
const pattern = param.type === "required_parameter" ||
|
|
2396
|
-
param.type === "optional_parameter"
|
|
2397
|
-
? param.childForFieldName?.("pattern")
|
|
2398
|
-
: param;
|
|
2399
|
-
if (pattern && this.patternBindsName(pattern, name))
|
|
2400
|
-
return true;
|
|
2401
|
-
}
|
|
2402
|
-
return false;
|
|
2403
|
-
}
|
|
2404
|
-
/**
|
|
2405
|
-
* True when a binding pattern (`identifier`, or a destructuring
|
|
2406
|
-
* object/array/rest pattern) introduces `name`. Walks the pattern but skips
|
|
2407
|
-
* `assignment_pattern` default values (`= expr`), which are uses.
|
|
2408
|
-
*/
|
|
2409
|
-
patternBindsName(pattern, name) {
|
|
2410
|
-
if (pattern.type === "identifier")
|
|
2411
|
-
return pattern.text === name;
|
|
2412
|
-
const stack = [pattern];
|
|
2413
|
-
while (stack.length > 0) {
|
|
2414
|
-
const n = stack.pop();
|
|
2415
|
-
if (!n)
|
|
2416
|
-
continue;
|
|
2417
|
-
if ((n.type === "identifier" ||
|
|
2418
|
-
n.type === "shorthand_property_identifier_pattern") &&
|
|
2419
|
-
n.text === name) {
|
|
2420
|
-
return true;
|
|
2421
|
-
}
|
|
2422
|
-
if (n.type === "assignment_pattern") {
|
|
2423
|
-
// Only the left (binding) side introduces names; skip the default.
|
|
2424
|
-
const left = n.childForFieldName?.("left") ?? n.children?.[0];
|
|
2425
|
-
if (left)
|
|
2426
|
-
stack.push(left);
|
|
2427
|
-
continue;
|
|
2428
|
-
}
|
|
2429
|
-
for (const c of n.children ?? [])
|
|
2430
|
-
stack.push(c);
|
|
2431
|
-
}
|
|
2432
|
-
return false;
|
|
2433
|
-
}
|
|
2434
|
-
/**
|
|
2435
|
-
* True when `name` resolves (same file) to a `const` initialized with
|
|
2436
|
-
* `new URL(<literalPath>, <fixedBase>)` — a fully fixed destination origin
|
|
2437
|
-
* and path. Query parameters added later via `url.searchParams.set(...)` do
|
|
2438
|
-
* not alter origin/path, so they never taint the destination. The `URL`
|
|
2439
|
-
* constructor may be imported under an alias (e.g. `NodeURL`).
|
|
2440
|
-
*/
|
|
2441
|
-
resolvesToFixedNewUrlConst(name, rootNode) {
|
|
2442
|
-
const value = this.resolveFileConstValueNode(name, rootNode);
|
|
2443
|
-
if (!value || value.type !== "new_expression")
|
|
2444
|
-
return false;
|
|
2445
|
-
const ctor = value.childForFieldName?.("constructor");
|
|
2446
|
-
const ctorName = ctor?.text ?? "";
|
|
2447
|
-
const isUrlCtor = ctorName === "URL" ||
|
|
2448
|
-
(ctor?.type === "identifier" &&
|
|
2449
|
-
this.importedAs(ctorName, rootNode) === "URL");
|
|
2450
|
-
if (!isUrlCtor)
|
|
2451
|
-
return false;
|
|
2452
|
-
const args = (value.childForFieldName?.("arguments")?.children ?? []).filter((c) => c.isNamed && c.type !== "comment");
|
|
2453
|
-
if (args.length === 0)
|
|
2454
|
-
return false;
|
|
2455
|
-
// First arg (relative path / full URL) must be a literal.
|
|
2456
|
-
if (!this.isFixedUrlLiteralExpr(args[0]))
|
|
2457
|
-
return false;
|
|
2458
|
-
// Single-arg form: `new URL("https://host/path")` — already fully fixed.
|
|
2459
|
-
if (args.length === 1)
|
|
2460
|
-
return true;
|
|
2461
|
-
// Two-arg form: base must resolve to a fixed origin.
|
|
2462
|
-
return this.isFixedUrlBaseExpr(args[1], rootNode);
|
|
2463
|
-
}
|
|
2464
|
-
/**
|
|
2465
|
-
* For a fetch URL argument of the form `u.toString()` (call_expression) or
|
|
2466
|
-
* `u.href` (member_expression), returns the receiver identifier name `u`
|
|
2467
|
-
* (only for the `toString`/`href` accessors a `URL` yields). Returns `null`
|
|
2468
|
-
* for any other shape so the caller falls through to taint heuristics.
|
|
2469
|
-
*/
|
|
2470
|
-
newUrlBaseVarName(urlNode) {
|
|
2471
|
-
let member;
|
|
2472
|
-
if (urlNode.type === "call_expression") {
|
|
2473
|
-
member = urlNode.childForFieldName?.("function");
|
|
2474
|
-
}
|
|
2475
|
-
else if (urlNode.type === "member_expression") {
|
|
2476
|
-
member = urlNode;
|
|
2477
|
-
}
|
|
2478
|
-
if (!member || member.type !== "member_expression")
|
|
2479
|
-
return null;
|
|
2480
|
-
const prop = member.childForFieldName?.("property")?.text;
|
|
2481
|
-
if (prop !== "toString" && prop !== "href")
|
|
2482
|
-
return null;
|
|
2483
|
-
const object = member.childForFieldName?.("object");
|
|
2484
|
-
if (object?.type !== "identifier")
|
|
2485
|
-
return null;
|
|
2486
|
-
return object.text;
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* `session.execute(select(...))` and friends pass a statement OBJECT, not a
|
|
2490
|
-
* SQL string: parameterized by construction, and far too noisy as blockers.
|
|
2491
|
-
*/
|
|
2492
|
-
isSafeSqlAlchemyExpressionCall(node) {
|
|
2493
|
-
if (node.type !== "call")
|
|
2494
|
-
return false;
|
|
2495
|
-
const callee = node.children?.[0]?.text ?? "";
|
|
2496
|
-
const expression = node.text;
|
|
2497
|
-
for (const name of PYTHON_SQLALCHEMY_STATEMENT_BUILDERS) {
|
|
2498
|
-
if (callee === name || expression.startsWith(`${name}(`))
|
|
2499
|
-
return true;
|
|
2500
|
-
}
|
|
2501
|
-
return false;
|
|
2502
|
-
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Post-filter predicate: returns true if the match should be kept, false to skip.
|
|
2505
|
-
* Each branch is an independent filter identified by name — flat dispatch, no nesting.
|
|
2506
|
-
*/
|
|
2507
|
-
// biome-ignore lint/suspicious/noExplicitAny: postFilterParams is untyped per-filter config
|
|
2508
|
-
applyPostFilter(postFilter, postFilterParams, captures, rootNode) {
|
|
2509
|
-
/**
|
|
2510
|
-
* Extract the list of declared slot names from a class_definition's
|
|
2511
|
-
* `__slots__` assignment. Returns:
|
|
2512
|
-
* - `null` if the class has no `__slots__` declaration
|
|
2513
|
-
* - an array of slot names (strings) otherwise
|
|
2514
|
-
*
|
|
2515
|
-
* Handles both common shapes:
|
|
2516
|
-
* - string tuple/list: `__slots__ = ("a", "b")` or `['a', 'b']`
|
|
2517
|
-
* - single string: `__slots__ = "a"` (Python's quirky single-slot form)
|
|
2518
|
-
* - parent inheritance: returns null (we don't follow MRO)
|
|
2519
|
-
*/
|
|
2520
|
-
function extractSlots(classNode) {
|
|
2521
|
-
const classText = classNode.text ?? "";
|
|
2522
|
-
if (!classText.includes("__slots__"))
|
|
2523
|
-
return null;
|
|
2524
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
2525
|
-
const body = classNode.children?.find((c) => c.type === "block");
|
|
2526
|
-
if (!body)
|
|
2527
|
-
return null;
|
|
2528
|
-
const slots = [];
|
|
2529
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
2530
|
-
for (const stmt of body.children ?? []) {
|
|
2531
|
-
if (stmt.type !== "expression_statement")
|
|
2532
|
-
continue;
|
|
2533
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST traversal
|
|
2534
|
-
const assignment = stmt.children?.find((c) => c.type === "assignment");
|
|
2535
|
-
if (!assignment)
|
|
2536
|
-
continue;
|
|
2537
|
-
// biome-ignore lint/suspicious/noExplicitAny: LHS check
|
|
2538
|
-
// LHS text may include a leading whitespace token from the AST
|
|
2539
|
-
// (tree-sitter separates the space before the LHS identifier).
|
|
2540
|
-
const lhsText = (assignment.children?.[0]?.text ?? "").trim();
|
|
2541
|
-
if (lhsText !== "__slots__")
|
|
2542
|
-
continue;
|
|
2543
|
-
// biome-ignore lint/suspicious/noExplicitAny: RHS extraction
|
|
2544
|
-
// children layout: [LHS identifier, `=` operator, RHS expression]
|
|
2545
|
-
const rhs = assignment.children?.[2];
|
|
2546
|
-
if (!rhs)
|
|
2547
|
-
continue;
|
|
2548
|
-
if (rhs.type === "string") {
|
|
2549
|
-
// __slots__ = "a" — single string form (Python quirk)
|
|
2550
|
-
const s = (rhs.text ?? "").replace(/^["']|["']$/g, "");
|
|
2551
|
-
if (s)
|
|
2552
|
-
slots.push(s);
|
|
2553
|
-
}
|
|
2554
|
-
else if (rhs.type === "tuple" || rhs.type === "list") {
|
|
2555
|
-
// biome-ignore lint/suspicious/noExplicitAny: list element extraction
|
|
2556
|
-
for (const el of rhs.children ?? []) {
|
|
2557
|
-
if (!el.isNamed)
|
|
2558
|
-
continue; // skip "," punctuation
|
|
2559
|
-
if (el.type === "string") {
|
|
2560
|
-
slots.push((el.text ?? "").replace(/^["']|["']$/g, ""));
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
break; // first __slots__ wins
|
|
2565
|
-
}
|
|
2566
|
-
return slots;
|
|
2567
|
-
}
|
|
2568
|
-
switch (postFilter) {
|
|
2569
|
-
case "no_nested_anchor_chain": {
|
|
2570
|
-
// Tree-sitter queries can express the opening-tag shape, but they
|
|
2571
|
-
// cannot express both an arbitrary-depth descendant relationship and
|
|
2572
|
-
// an ancestor exclusion. Walk the captured JSX element here so the
|
|
2573
|
-
// TSX path agrees with the ast-grep rule: keep exactly the outermost
|
|
2574
|
-
// anchor that contains another anchor, including through wrappers.
|
|
2575
|
-
// This post-filter runs inside a batched query walk. It must never
|
|
2576
|
-
// abort that walk or turn a malformed tree into a false negative.
|
|
2577
|
-
try {
|
|
2578
|
-
const outer = captures.OUTER_ELEMENT;
|
|
2579
|
-
if (!outer) {
|
|
2580
|
-
this.reportPostFilterFailure(postFilter, "missing OUTER_ELEMENT capture");
|
|
2581
|
-
return true;
|
|
2582
|
-
}
|
|
2583
|
-
const isAnchorElement = (node) => {
|
|
2584
|
-
if (node.type !== "jsx_element")
|
|
2585
|
-
return false;
|
|
2586
|
-
const opening = node.childForFieldName?.("open_tag") ??
|
|
2587
|
-
node.children?.find((child) => child.type === "jsx_opening_element");
|
|
2588
|
-
const name = opening?.childForFieldName?.("name") ??
|
|
2589
|
-
opening?.children?.find((child) => child.type === "identifier");
|
|
2590
|
-
return name?.text === "a";
|
|
2591
|
-
};
|
|
2592
|
-
let visited = 0;
|
|
2593
|
-
let ancestor = outer.parent;
|
|
2594
|
-
while (ancestor) {
|
|
2595
|
-
if (++visited > NO_NESTED_ANCHOR_VISIT_CAP) {
|
|
2596
|
-
this.reportPostFilterFailure(postFilter, `ancestor walk exceeded ${NO_NESTED_ANCHOR_VISIT_CAP} nodes`);
|
|
2597
|
-
return true;
|
|
2598
|
-
}
|
|
2599
|
-
if (isAnchorElement(ancestor))
|
|
2600
|
-
return false;
|
|
2601
|
-
ancestor = ancestor.parent;
|
|
2602
|
-
}
|
|
2603
|
-
const pending = [...(outer.children ?? [])];
|
|
2604
|
-
while (pending.length > 0) {
|
|
2605
|
-
const node = pending.pop();
|
|
2606
|
-
if (!node)
|
|
2607
|
-
continue;
|
|
2608
|
-
if (++visited > NO_NESTED_ANCHOR_VISIT_CAP) {
|
|
2609
|
-
this.reportPostFilterFailure(postFilter, `descendant walk exceeded ${NO_NESTED_ANCHOR_VISIT_CAP} nodes`);
|
|
2610
|
-
return true;
|
|
2611
|
-
}
|
|
2612
|
-
if (isAnchorElement(node))
|
|
2613
|
-
return true;
|
|
2614
|
-
pending.push(...(node.children ?? []));
|
|
2615
|
-
}
|
|
2616
|
-
return false;
|
|
2617
|
-
}
|
|
2618
|
-
catch (error) {
|
|
2619
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
2620
|
-
this.reportPostFilterFailure(postFilter, `tree walk failed${reason ? `: ${reason}` : ""}`);
|
|
2621
|
-
return true;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
case "differs_only_by_case": {
|
|
2625
|
-
try {
|
|
2626
|
-
const field = captures.FIELD?.text ?? "";
|
|
2627
|
-
const method = captures.METHOD?.text ?? "";
|
|
2628
|
-
return (field !== method &&
|
|
2629
|
-
field.toLocaleLowerCase() === method.toLocaleLowerCase());
|
|
2630
|
-
}
|
|
2631
|
-
catch {
|
|
2632
|
-
return true;
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
case "in_default_package": {
|
|
2636
|
-
try {
|
|
2637
|
-
if (!rootNode)
|
|
2638
|
-
return true;
|
|
2639
|
-
return !(rootNode.children ?? []).some((node) => node.type === "package_declaration");
|
|
2640
|
-
}
|
|
2641
|
-
catch {
|
|
2642
|
-
return true;
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
case "missing_mimetype_and_download_name": {
|
|
2646
|
-
try {
|
|
2647
|
-
const firstArg = captures.FIRST_ARG;
|
|
2648
|
-
if (!firstArg)
|
|
2649
|
-
return true;
|
|
2650
|
-
// Flask can infer a MIME type from a filesystem path. The warning
|
|
2651
|
-
// is for file-like objects, where it needs an explicit MIME type or
|
|
2652
|
-
// download name.
|
|
2653
|
-
if (firstArg.type === "string")
|
|
2654
|
-
return false;
|
|
2655
|
-
const args = firstArg.parent;
|
|
2656
|
-
if (!args)
|
|
2657
|
-
return true;
|
|
2658
|
-
return !(args.children ?? []).some((node) => {
|
|
2659
|
-
if (node.type !== "keyword_argument")
|
|
2660
|
-
return false;
|
|
2661
|
-
const name = node.childForFieldName?.("name")?.text;
|
|
2662
|
-
return name === "mimetype" || name === "download_name";
|
|
2663
|
-
});
|
|
2664
|
-
}
|
|
2665
|
-
catch {
|
|
2666
|
-
return true;
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
case "missing_super_call": {
|
|
2670
|
-
try {
|
|
2671
|
-
const body = captures.BODY;
|
|
2672
|
-
const method = captures.METHOD?.text ?? "";
|
|
2673
|
-
if (!body || !method)
|
|
2674
|
-
return true;
|
|
2675
|
-
const stack = [body];
|
|
2676
|
-
for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
|
|
2677
|
-
const node = stack.pop();
|
|
2678
|
-
if (!node)
|
|
2679
|
-
break;
|
|
2680
|
-
if (node.type === "method_invocation" &&
|
|
2681
|
-
node.childForFieldName?.("object")?.text === "super" &&
|
|
2682
|
-
node.childForFieldName?.("name")?.text === method) {
|
|
2683
|
-
return false;
|
|
2684
|
-
}
|
|
2685
|
-
stack.push(...(node.children ?? []));
|
|
2686
|
-
}
|
|
2687
|
-
return true;
|
|
2688
|
-
}
|
|
2689
|
-
catch {
|
|
2690
|
-
return true;
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
case "no_assertion_call": {
|
|
2694
|
-
try {
|
|
2695
|
-
const body = captures.BODY;
|
|
2696
|
-
if (!body)
|
|
2697
|
-
return true;
|
|
2698
|
-
const assertionNames = /^(?:assert\w*|fail|verify|expect|check|assume\w*)$/i;
|
|
2699
|
-
const stack = [body];
|
|
2700
|
-
for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
|
|
2701
|
-
const node = stack.pop();
|
|
2702
|
-
if (!node)
|
|
2703
|
-
break;
|
|
2704
|
-
if (node.type === "method_invocation") {
|
|
2705
|
-
const name = node.childForFieldName?.("name")?.text ?? "";
|
|
2706
|
-
if (assertionNames.test(name))
|
|
2707
|
-
return false;
|
|
2708
|
-
}
|
|
2709
|
-
stack.push(...(node.children ?? []));
|
|
2710
|
-
}
|
|
2711
|
-
return true;
|
|
2712
|
-
}
|
|
2713
|
-
catch {
|
|
2714
|
-
return true;
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
case "not_closed_or_try_with_resources": {
|
|
2718
|
-
try {
|
|
2719
|
-
const declaration = captures.DECL;
|
|
2720
|
-
const resource = captures.RESOURCE?.text ?? "";
|
|
2721
|
-
if (!declaration || !resource)
|
|
2722
|
-
return true;
|
|
2723
|
-
// (#956 review: the old resource_specification ANCESTOR walk was
|
|
2724
|
-
// dead code — the query only matches local_variable_declaration,
|
|
2725
|
-
// which the Java grammar never places inside a try header. The
|
|
2726
|
-
// real try-with-resources shapes are handled in the scope scan
|
|
2727
|
-
// below: a `resource` node naming this variable covers both
|
|
2728
|
-
// `try (Type x = …)` and the Java 9 `try (x)` form.)
|
|
2729
|
-
const scope = this.navigator.findParent(declaration, [
|
|
2730
|
-
"method_declaration",
|
|
2731
|
-
"constructor_declaration",
|
|
2732
|
-
"block",
|
|
2733
|
-
]) ?? rootNode;
|
|
2734
|
-
if (!scope)
|
|
2735
|
-
return true;
|
|
2736
|
-
// `\b` in a template literal is the BACKSPACE control char
|
|
2737
|
-
// (U+0008), not a regex word-boundary escape — that requires
|
|
2738
|
-
// the literal two-character sequence `\\b`. The identifier
|
|
2739
|
-
// itself must also be regex-escaped (#1089 P2).
|
|
2740
|
-
const resourceWord = new RegExp(`\\b${escapeRegExp(resource)}\\b`);
|
|
2741
|
-
const stack = [scope];
|
|
2742
|
-
for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
|
|
2743
|
-
const node = stack.pop();
|
|
2744
|
-
if (!node)
|
|
2745
|
-
break;
|
|
2746
|
-
if (node.type === "method_invocation" &&
|
|
2747
|
-
node.childForFieldName?.("object")?.text === resource &&
|
|
2748
|
-
node.childForFieldName?.("name")?.text === "close") {
|
|
2749
|
-
return false;
|
|
2750
|
-
}
|
|
2751
|
-
if (node.type === "resource" &&
|
|
2752
|
-
resourceWord.test(node.text ?? "")) {
|
|
2753
|
-
return false;
|
|
2754
|
-
}
|
|
2755
|
-
stack.push(...(node.children ?? []));
|
|
2756
|
-
}
|
|
2757
|
-
return true;
|
|
2758
|
-
}
|
|
2759
|
-
catch {
|
|
2760
|
-
return true;
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
case "same_method_no_base_case": {
|
|
2764
|
-
try {
|
|
2765
|
-
const method = captures.NAME?.text ?? "";
|
|
2766
|
-
if (!method || captures.RECURSE?.text !== method)
|
|
2767
|
-
return false;
|
|
2768
|
-
const call = captures.CALL;
|
|
2769
|
-
const declaration = call
|
|
2770
|
-
? this.navigator.findParent(call, ["method_declaration"])
|
|
2771
|
-
: undefined;
|
|
2772
|
-
if (!declaration)
|
|
2773
|
-
return true;
|
|
2774
|
-
const stack = [declaration];
|
|
2775
|
-
for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
|
|
2776
|
-
const node = stack.pop();
|
|
2777
|
-
if (!node)
|
|
2778
|
-
break;
|
|
2779
|
-
// Any conditional or loop construct is a plausible base-case
|
|
2780
|
-
// guard (#956 review: while/for-guarded recursion and ternary
|
|
2781
|
-
// base cases are common). This errs toward suppressing a
|
|
2782
|
-
// blocking diagnostic, never inventing one.
|
|
2783
|
-
if (node.type === "if_statement" ||
|
|
2784
|
-
node.type === "switch_expression" ||
|
|
2785
|
-
node.type === "switch_statement" ||
|
|
2786
|
-
node.type === "while_statement" ||
|
|
2787
|
-
node.type === "do_statement" ||
|
|
2788
|
-
node.type === "for_statement" ||
|
|
2789
|
-
node.type === "enhanced_for_statement" ||
|
|
2790
|
-
node.type === "ternary_expression") {
|
|
2791
|
-
return false;
|
|
2792
|
-
}
|
|
2793
|
-
stack.push(...(node.children ?? []));
|
|
2794
|
-
}
|
|
2795
|
-
// Cap exhausted without a verdict: this rule is BLOCKING, so a
|
|
2796
|
-
// >10k-node method whose guard sits beyond the budget must not
|
|
2797
|
-
// become a silent blocking FP — suppress instead (#956 review;
|
|
2798
|
-
// the advisory filters keep their keep-the-diagnostic default).
|
|
2799
|
-
return stack.length > 0 ? false : true;
|
|
2800
|
-
}
|
|
2801
|
-
catch {
|
|
2802
|
-
return true;
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
case "memset_for_sensitive_data": {
|
|
2806
|
-
try {
|
|
2807
|
-
const destination = captures.DEST?.text ?? "";
|
|
2808
|
-
const value = captures.VALUE?.text?.trim() ?? "";
|
|
2809
|
-
if (!/^(?:0|0x0+|NULL|nullptr)$/.test(value))
|
|
2810
|
-
return false;
|
|
2811
|
-
return /(?:pass(?:word)?|passwd|secret|token|api_?key|private_?key|credential|auth|pin)/i.test(destination);
|
|
2812
|
-
}
|
|
2813
|
-
catch {
|
|
2814
|
-
return true;
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
case "unsafe_regex_dynamic_identifier": {
|
|
2818
|
-
// unsafe-regex is intentionally a coarse advisory heuristic. When the
|
|
2819
|
-
// interpolation is a plain identifier, recover the common safe-before-
|
|
2820
|
-
// assignment pattern without pretending to perform whole-program
|
|
2821
|
-
// dataflow. #949 review hardening: the LAST write (declarator or
|
|
2822
|
-
// assignment) textually BEFORE the use decides — a later reassignment
|
|
2823
|
-
// to user input un-suppresses, and an unrelated declaration after the
|
|
2824
|
-
// use never suppresses. Initializers come from the "value" field (a
|
|
2825
|
-
// type annotation is not an initializer), and bare .replace("a","b")
|
|
2826
|
-
// is not escaping — only escape-named callees or .replace(/regex/
|
|
2827
|
-
// count. Any internal error keeps the diagnostic: never silently
|
|
2828
|
-
// suppress via a filter crash, and a throw here would abort the whole
|
|
2829
|
-
// file's batch.
|
|
2830
|
-
try {
|
|
2831
|
-
const interpolationNode = captures.INTERPOLATION;
|
|
2832
|
-
const interpolation = interpolationNode?.text ?? "";
|
|
2833
|
-
const identifier = interpolation.match(/^\$\{\s*([A-Za-z_$][\w$]*)\s*\}$/)?.[1];
|
|
2834
|
-
if (!identifier || !rootNode || !interpolationNode)
|
|
2835
|
-
return true;
|
|
2836
|
-
const useRow = interpolationNode.startPosition.row;
|
|
2837
|
-
const hasEscapeSignal = (text) => /\b(?:escape|Escape)\w*\s*\(/.test(text) ||
|
|
2838
|
-
/\.\s*replace\s*\(\s*\//.test(text);
|
|
2839
|
-
// Track the last write to the identifier before the use site.
|
|
2840
|
-
let lastWriteRow = -1;
|
|
2841
|
-
let lastWriteSafe = false;
|
|
2842
|
-
const consider = (row, valueText) => {
|
|
2843
|
-
if (row >= useRow || row < lastWriteRow)
|
|
2844
|
-
return;
|
|
2845
|
-
lastWriteRow = row;
|
|
2846
|
-
lastWriteSafe =
|
|
2847
|
-
valueText !== undefined && hasEscapeSignal(valueText);
|
|
2848
|
-
};
|
|
2849
|
-
const stack = [rootNode];
|
|
2850
|
-
while (stack.length > 0) {
|
|
2851
|
-
const node = stack.pop();
|
|
2852
|
-
if (!node)
|
|
2853
|
-
continue;
|
|
2854
|
-
if (node.type === "variable_declarator") {
|
|
2855
|
-
const name = node.childForFieldName?.("name");
|
|
2856
|
-
if (name?.text === identifier) {
|
|
2857
|
-
const initializer = node.childForFieldName?.("value");
|
|
2858
|
-
consider(node.startPosition.row, initializer?.text);
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
else if (node.type === "assignment_expression") {
|
|
2862
|
-
const left = node.childForFieldName?.("left");
|
|
2863
|
-
if (left?.type === "identifier" && left.text === identifier) {
|
|
2864
|
-
const right = node.childForFieldName?.("right");
|
|
2865
|
-
consider(node.startPosition.row, right?.text);
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
stack.push(...node.children);
|
|
2869
|
-
}
|
|
2870
|
-
return lastWriteRow >= 0 && lastWriteSafe ? false : true;
|
|
2871
|
-
}
|
|
2872
|
-
catch {
|
|
2873
|
-
return true;
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
case "is_generator_with_valued_return": {
|
|
2877
|
-
const returnNode = captures.RETURN;
|
|
2878
|
-
const functionNode = captures.FUNCTION ??
|
|
2879
|
-
(returnNode
|
|
2880
|
-
? this.navigator.findParent(returnNode, ["function_definition"])
|
|
2881
|
-
: undefined);
|
|
2882
|
-
if (!functionNode)
|
|
2883
|
-
return false;
|
|
2884
|
-
// In the Python grammar, `async def` is also a function_definition with
|
|
2885
|
-
// an anonymous `async` child. Coroutines may return values normally;
|
|
2886
|
-
// only synchronous generator functions should be flagged.
|
|
2887
|
-
if (this.hasChildToken(functionNode, "async"))
|
|
2888
|
-
return false;
|
|
2889
|
-
return this.containsYieldInFunctionBody(functionNode);
|
|
2890
|
-
}
|
|
2891
|
-
case "count_params": {
|
|
2892
|
-
const paramsNode = captures.PARAMS;
|
|
2893
|
-
if (!paramsNode)
|
|
2894
|
-
return true;
|
|
2895
|
-
// Count only truly required params — exclude:
|
|
2896
|
-
// • optional_parameter nodes (foo?: T) if the grammar uses that type
|
|
2897
|
-
// • required_parameter nodes that have a "?" child (same semantic,
|
|
2898
|
-
// different grammar version — web-tree-sitter-typescript collapses
|
|
2899
|
-
// both into required_parameter with a "?" token child)
|
|
2900
|
-
// • params with a default value (text contains "=")
|
|
2901
|
-
// biome-ignore lint/suspicious/noExplicitAny: Count parameter nodes
|
|
2902
|
-
const paramCount = paramsNode.children.filter((c) => {
|
|
2903
|
-
if (c.type !== "required_parameter")
|
|
2904
|
-
return false;
|
|
2905
|
-
if (c.text.includes("="))
|
|
2906
|
-
return false;
|
|
2907
|
-
// biome-ignore lint/suspicious/noExplicitAny: child node check
|
|
2908
|
-
if (c.children?.some((ch) => ch.text === "?"))
|
|
2909
|
-
return false;
|
|
2910
|
-
return true;
|
|
2911
|
-
}).length;
|
|
2912
|
-
return paramCount >= (postFilterParams?.min_params ?? 6);
|
|
2913
|
-
}
|
|
2914
|
-
case "empty_body": {
|
|
2915
|
-
const bodyNode = captures.BODY;
|
|
2916
|
-
if (!bodyNode)
|
|
2917
|
-
return true;
|
|
2918
|
-
// biome-ignore lint/suspicious/noExplicitAny: Check for meaningful statements
|
|
2919
|
-
const meaningful = bodyNode.children.filter((c) => c.isNamed &&
|
|
2920
|
-
c.type !== "comment" &&
|
|
2921
|
-
c.type !== "line_comment" &&
|
|
2922
|
-
c.type !== "block_comment");
|
|
2923
|
-
return meaningful.length === 0;
|
|
2924
|
-
}
|
|
2925
|
-
case "is_empty_block": {
|
|
2926
|
-
// empty-switch-case: keep a `switch_case` that has no body statements.
|
|
2927
|
-
// A switch_case's named children are `[value, ...body statements]`
|
|
2928
|
-
// (the case's statements are direct children, not a statement_block),
|
|
2929
|
-
// so an empty case is one whose only named child is its `case <value>`.
|
|
2930
|
-
// An empty case followed by another label is a fall-through group, not
|
|
2931
|
-
// a dead case — only the last label of a group carries the body.
|
|
2932
|
-
const caseNode = captures.CASE;
|
|
2933
|
-
if (!caseNode)
|
|
2934
|
-
return false;
|
|
2935
|
-
if (this.switchCaseBodyStatements(caseNode).length > 0)
|
|
2936
|
-
return false;
|
|
2937
|
-
return !this.nextSiblingIsSwitchLabel(caseNode);
|
|
2938
|
-
}
|
|
2939
|
-
case "no_break_or_return_in_body": {
|
|
2940
|
-
// infinite-loop: keep a `while(true)`/`for(;;)` whose body contains no
|
|
2941
|
-
// statement that can terminate the loop (break/return/throw). `continue`
|
|
2942
|
-
// does not count — it keeps the loop running.
|
|
2943
|
-
const bodyNode = captures.BODY;
|
|
2944
|
-
if (!bodyNode)
|
|
2945
|
-
return false;
|
|
2946
|
-
return !this.bodyHasLoopExit(bodyNode, false);
|
|
2947
|
-
}
|
|
2948
|
-
case "same_param_name": {
|
|
2949
|
-
// duplicate-function-arg: keep the pair when the captured binding
|
|
2950
|
-
// patterns share any name.
|
|
2951
|
-
const first = this.bindingNames(captures.PARAM1);
|
|
2952
|
-
const second = this.bindingNames(captures.NAME);
|
|
2953
|
-
return [...first].some((name) => second.has(name));
|
|
2954
|
-
}
|
|
2955
|
-
case "no_terminating_statement": {
|
|
2956
|
-
// switch-case-termination: keep a non-empty case (already known to be
|
|
2957
|
-
// followed by another case via the query) whose last body statement
|
|
2958
|
-
// does not terminate, i.e. it falls through. Empty cases are handled
|
|
2959
|
-
// by empty-switch-case, so require at least one body statement here.
|
|
2960
|
-
// An intentional `// fallthrough` marker suppresses the finding.
|
|
2961
|
-
const caseNode = captures.CASE;
|
|
2962
|
-
if (!caseNode)
|
|
2963
|
-
return false;
|
|
2964
|
-
if (this.hasFallthroughMarker(caseNode))
|
|
2965
|
-
return false;
|
|
2966
|
-
const body = this.switchCaseBodyStatements(caseNode);
|
|
2967
|
-
if (body.length === 0)
|
|
2968
|
-
return false;
|
|
2969
|
-
return !this.statementTerminates(body[body.length - 1]);
|
|
2970
|
-
}
|
|
2971
|
-
case "no_break_or_return": {
|
|
2972
|
-
// infinite-loop-java: keep a `while(true)`/`for(;;)` whose body has no
|
|
2973
|
-
// break/return/throw reachable in this loop's scope.
|
|
2974
|
-
const bodyNode = captures.BODY;
|
|
2975
|
-
if (!bodyNode)
|
|
2976
|
-
return false;
|
|
2977
|
-
return !this.bodyHasLoopExit(bodyNode, false);
|
|
2978
|
-
}
|
|
2979
|
-
case "is_double_checked_locking": {
|
|
2980
|
-
// no-double-checked-locking: the query pins the if→synchronized→if
|
|
2981
|
-
// shape; keep it only when both null-checks test the same field.
|
|
2982
|
-
const outer = captures.FIELD?.text;
|
|
2983
|
-
const inner = captures.FIELD2?.text;
|
|
2984
|
-
return !!outer && outer === inner;
|
|
2985
|
-
}
|
|
2986
|
-
case "shadows_parent_field": {
|
|
2987
|
-
// no-field-shadowing: keep only when the named parent class is
|
|
2988
|
-
// declared in the same file and it declares a field with the same
|
|
2989
|
-
// name. Cross-file inheritance can't be resolved here — fail closed.
|
|
2990
|
-
const parentName = captures.PARENT?.text;
|
|
2991
|
-
const fieldName = captures.NAME?.text;
|
|
2992
|
-
if (!parentName || !fieldName)
|
|
2993
|
-
return false;
|
|
2994
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST traversal
|
|
2995
|
-
let root = captures.NAME;
|
|
2996
|
-
while (root.parent)
|
|
2997
|
-
root = root.parent;
|
|
2998
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST traversal
|
|
2999
|
-
const stack = [root];
|
|
3000
|
-
while (stack.length) {
|
|
3001
|
-
const node = stack.pop();
|
|
3002
|
-
if (node.type === "class_declaration" &&
|
|
3003
|
-
node.childForFieldName?.("name")?.text === parentName) {
|
|
3004
|
-
const body = node.childForFieldName?.("body");
|
|
3005
|
-
for (const member of body?.children ?? []) {
|
|
3006
|
-
if (member.type !== "field_declaration")
|
|
3007
|
-
continue;
|
|
3008
|
-
for (const declarator of member.children ?? []) {
|
|
3009
|
-
if (declarator.type === "variable_declarator" &&
|
|
3010
|
-
declarator.childForFieldName?.("name")?.text === fieldName) {
|
|
3011
|
-
return true;
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
for (const child of node.children ?? [])
|
|
3017
|
-
stack.push(child);
|
|
3018
|
-
}
|
|
3019
|
-
return false;
|
|
3020
|
-
}
|
|
3021
|
-
case "missing_break_between_cases": {
|
|
3022
|
-
// switch-fall-through: keep when the labeled statement group contains
|
|
3023
|
-
// no terminating statement at all — its control flow reaches the next
|
|
3024
|
-
// case. The query already requires a following group.
|
|
3025
|
-
const label = captures.LABEL;
|
|
3026
|
-
const group = label?.parent;
|
|
3027
|
-
if (!group)
|
|
3028
|
-
return false;
|
|
3029
|
-
const TERMINATORS = new Set([
|
|
3030
|
-
"break_statement",
|
|
3031
|
-
"return_statement",
|
|
3032
|
-
"throw_statement",
|
|
3033
|
-
"continue_statement",
|
|
3034
|
-
"yield_statement",
|
|
3035
|
-
]);
|
|
3036
|
-
for (const child of group.children ?? []) {
|
|
3037
|
-
if (TERMINATORS.has(child.type))
|
|
3038
|
-
return false;
|
|
3039
|
-
}
|
|
3040
|
-
return true;
|
|
3041
|
-
}
|
|
3042
|
-
case "scoped_lock_empty_args": {
|
|
3043
|
-
// no-scoped-lock-without-args: the query only matches a bare
|
|
3044
|
-
// `declarator: (identifier)` — an arg-taking declaration parses as an
|
|
3045
|
-
// init/function declarator instead — so a match IS the defect. Keep it
|
|
3046
|
-
// as long as the captured declarator has no sibling argument list.
|
|
3047
|
-
const decl = captures.DECL;
|
|
3048
|
-
if (!decl)
|
|
3049
|
-
return false;
|
|
3050
|
-
for (const sibling of decl.parent?.children ?? []) {
|
|
3051
|
-
if (sibling.type === "argument_list")
|
|
3052
|
-
return false;
|
|
3053
|
-
}
|
|
3054
|
-
return true;
|
|
3055
|
-
}
|
|
3056
|
-
case "calc_missing_spaces": {
|
|
3057
|
-
// calc-spacing: keep a calc() whose +/- has an operand directly on
|
|
3058
|
-
// both sides (e.g. `100%-20px`). A leading sign after `(` or a comma
|
|
3059
|
-
// is legitimate and stays allowed.
|
|
3060
|
-
const text = captures.EXPR?.text ?? "";
|
|
3061
|
-
return /[\w%)][+-][\w.(]/.test(text);
|
|
3062
|
-
}
|
|
3063
|
-
case "bare_except_only": {
|
|
3064
|
-
const clauseNode = captures.CLAUSE;
|
|
3065
|
-
if (!clauseNode)
|
|
3066
|
-
return true;
|
|
3067
|
-
// A typed `except` clause has a named child for the exception
|
|
3068
|
-
// spec — one of: identifier (e.g. `except ValueError`),
|
|
3069
|
-
// attribute (e.g. `except asyncio.TimeoutError` — dotted name),
|
|
3070
|
-
// tuple (e.g. `except (E, F)`), as_pattern (e.g. `except E as e`),
|
|
3071
|
-
// parenthesized_expression (e.g. `except (E)`), or subscript
|
|
3072
|
-
// (e.g. `except dict[str, int]` — #1244, mirrors the ast-grep
|
|
3073
|
-
// rule's shape space).
|
|
3074
|
-
// Bare `except:` has NO named children (just the `except` keyword,
|
|
3075
|
-
// the `:` colon, and the body block).
|
|
3076
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
3077
|
-
const hasExceptionSpec = clauseNode.children.some((c) => {
|
|
3078
|
-
if (!c.isNamed)
|
|
3079
|
-
return false;
|
|
3080
|
-
return c.type !== "block";
|
|
3081
|
-
});
|
|
3082
|
-
// Fire ONLY when bare (no exception spec)
|
|
3083
|
-
return !hasExceptionSpec;
|
|
3084
|
-
}
|
|
3085
|
-
case "eq_mod_fn": {
|
|
3086
|
-
// Belt-and-braces re-check of the #eq? predicates that
|
|
3087
|
-
// evaluatePredicates already enforces (clients/tree-sitter-client.ts).
|
|
3088
|
-
// This filter re-applies the #eq? checks at post_filter time.
|
|
3089
|
-
const mod = captures.MOD?.text ?? "";
|
|
3090
|
-
const fn = captures.FN?.text ?? "";
|
|
3091
|
-
return mod === "threading" && fn === "Thread";
|
|
3092
|
-
}
|
|
3093
|
-
case "regex_first_arg_identifier": {
|
|
3094
|
-
// Belt-and-braces re-check of the #eq?/#match? predicates that
|
|
3095
|
-
// evaluatePredicates already enforces, plus the first-argument
|
|
3096
|
-
// check that predicates can't express. This post_filter re-applies
|
|
3097
|
-
// both predicate checks AND the first-argument check:
|
|
3098
|
-
// 1. MOD must be "re" (would-be #eq? @MOD "re")
|
|
3099
|
-
// 2. FUNC must match the regex method pattern (#match? @FUNC ...)
|
|
3100
|
-
// 3. First arg must be an identifier (dynamic pattern)
|
|
3101
|
-
// String literals (r"...", "...") are safe static patterns.
|
|
3102
|
-
const mod = captures.MOD?.text ?? "";
|
|
3103
|
-
if (mod !== "re")
|
|
3104
|
-
return false;
|
|
3105
|
-
const func = captures.FUNC?.text ?? "";
|
|
3106
|
-
if (!/^(compile|match|search|fullmatch|findall|finditer|sub|subn|split)$/.test(func)) {
|
|
3107
|
-
return false;
|
|
3108
|
-
}
|
|
3109
|
-
const argsNode = captures.ARGS;
|
|
3110
|
-
if (!argsNode)
|
|
3111
|
-
return false;
|
|
3112
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
3113
|
-
const firstNamed = (argsNode.children ?? []).find((c) => c.isNamed);
|
|
3114
|
-
if (!firstNamed)
|
|
3115
|
-
return false;
|
|
3116
|
-
return firstNamed.type === "identifier";
|
|
3117
|
-
}
|
|
3118
|
-
case "open_mode_invalid": {
|
|
3119
|
-
const modeNode = captures.MODE;
|
|
3120
|
-
if (!modeNode)
|
|
3121
|
-
return false;
|
|
3122
|
-
// Python's open() mode accepts: r, w, a, x (basic), b/t/+ (suffix).
|
|
3123
|
-
// Strip surrounding quotes from the string literal text.
|
|
3124
|
-
const text = modeNode.text ?? "";
|
|
3125
|
-
const stripped = text.replace(/^["']|["']$/g, "");
|
|
3126
|
-
// Skip empty mode (defaults to 'r')
|
|
3127
|
-
if (stripped.length === 0)
|
|
3128
|
-
return false;
|
|
3129
|
-
// Skip single-char modes (r/w/a/x — always valid)
|
|
3130
|
-
if (stripped.length === 1)
|
|
3131
|
-
return false;
|
|
3132
|
-
// Must contain only valid characters
|
|
3133
|
-
if (!/^[rwxabt+]+$/.test(stripped))
|
|
3134
|
-
return true;
|
|
3135
|
-
// Multi-char must be exactly: basic + optional (b|t) + optional +
|
|
3136
|
-
// Examples valid: "rb", "rb+", "r+", "ab", "rt"
|
|
3137
|
-
// Examples invalid: "rwb", "rrr", "rw", "rbb" (no + between r and w is invalid)
|
|
3138
|
-
// The "rw" case (basic mode followed by another basic mode without +) is invalid
|
|
3139
|
-
// Allow: [basic][bt]?[+]
|
|
3140
|
-
const validShape = /^[rwax][bt]?\+?$/;
|
|
3141
|
-
if (!validShape.test(stripped))
|
|
3142
|
-
return true;
|
|
3143
|
-
return false;
|
|
3144
|
-
}
|
|
3145
|
-
case "status_204_with_value_return": {
|
|
3146
|
-
const funcNode = captures.FUNC;
|
|
3147
|
-
const valNode = captures.VAL;
|
|
3148
|
-
if (!funcNode || !valNode)
|
|
3149
|
-
return false;
|
|
3150
|
-
// Only fire if status_code=204
|
|
3151
|
-
if (Number(valNode.text ?? 0) !== 204)
|
|
3152
|
-
return false;
|
|
3153
|
-
// Walk the function subtree looking for return_statement nodes.
|
|
3154
|
-
// Manual BFS because web-tree-sitter doesn't expose
|
|
3155
|
-
// descendantsOfType directly.
|
|
3156
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter node iteration
|
|
3157
|
-
const queue = [funcNode];
|
|
3158
|
-
while (queue.length > 0) {
|
|
3159
|
-
const node = queue.shift();
|
|
3160
|
-
if (node.type === "return_statement") {
|
|
3161
|
-
// Has a value child (not just the `return` keyword)
|
|
3162
|
-
// biome-ignore lint/suspicious/noExplicitAny: child check
|
|
3163
|
-
const hasValue = node.children.some((c) => c.isNamed && c.type !== "comment");
|
|
3164
|
-
if (hasValue)
|
|
3165
|
-
return true;
|
|
3166
|
-
}
|
|
3167
|
-
// biome-ignore lint/suspicious/noExplicitAny: child queue
|
|
3168
|
-
if (node.children)
|
|
3169
|
-
queue.push(...node.children);
|
|
3170
|
-
}
|
|
3171
|
-
return false;
|
|
3172
|
-
}
|
|
3173
|
-
case "has_mixed_async": {
|
|
3174
|
-
const bodyNode = captures.BODY;
|
|
3175
|
-
if (!bodyNode)
|
|
3176
|
-
return true;
|
|
3177
|
-
const bodyText = bodyNode.text;
|
|
3178
|
-
return (bodyText.includes("await") && /\.\s*(then|catch)\s*\(/.test(bodyText));
|
|
3179
|
-
}
|
|
3180
|
-
case "format_arity_mismatch": {
|
|
3181
|
-
const formatNode = captures.FORMAT;
|
|
3182
|
-
const argsNode = captures.ARGS;
|
|
3183
|
-
if (!formatNode || !argsNode)
|
|
3184
|
-
return false;
|
|
3185
|
-
// Strip quotes from format string
|
|
3186
|
-
const fmtText = (formatNode.text ?? "").replace(/^["']|["']$/g, "");
|
|
3187
|
-
// Don't strip a leading "%" — the format string's contents are
|
|
3188
|
-
// intact after stripping only the surrounding quotes. The original
|
|
3189
|
-
// code stripped the first "%" thinking it was the operator, but
|
|
3190
|
-
// the operator is a separate binary_operator node, not part of
|
|
3191
|
-
// the string literal's text.
|
|
3192
|
-
const fmt = fmtText;
|
|
3193
|
-
// Count placeholders: %s, %d, %f, %(name)s, %i, etc.
|
|
3194
|
-
// The simple %s/%d style: each %X counts as 1
|
|
3195
|
-
// The %(name)s style: counts as 1 with name
|
|
3196
|
-
// The %% escape: doesn't count
|
|
3197
|
-
let placeholderCount = 0;
|
|
3198
|
-
const namedKeys = [];
|
|
3199
|
-
// biome-ignore lint/suspicious/noExplicitAny: regex match
|
|
3200
|
-
const positionalRegex = /%(?:\([^)]+\))?[#0\- +]*\d*(?:\.\d+)?[hlL]?[diouxXeEfFgGcrs%]/g;
|
|
3201
|
-
// biome-ignore lint/suspicious/noExplicitAny: regex match
|
|
3202
|
-
const positionalMatches = fmt.match(positionalRegex) ?? [];
|
|
3203
|
-
for (const m of positionalMatches) {
|
|
3204
|
-
if (m === "%%")
|
|
3205
|
-
continue;
|
|
3206
|
-
placeholderCount++;
|
|
3207
|
-
// biome-ignore lint/suspicious/noExplicitAny: capture group
|
|
3208
|
-
const namedMatch = m.match(/^%\(([^)]+)\)/);
|
|
3209
|
-
if (namedMatch)
|
|
3210
|
-
namedKeys.push(namedMatch[1]);
|
|
3211
|
-
}
|
|
3212
|
-
// If format uses named placeholders, RHS should be a dict
|
|
3213
|
-
if (namedKeys.length > 0) {
|
|
3214
|
-
// Check if dict contains all named keys
|
|
3215
|
-
if (argsNode.type === "dictionary") {
|
|
3216
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
3217
|
-
const dictKeys = [];
|
|
3218
|
-
for (const child of argsNode.children ?? []) {
|
|
3219
|
-
// biome-ignore lint/suspicious/noExplicitAny: child check
|
|
3220
|
-
if (child.type === "pair" && child.children?.[0]) {
|
|
3221
|
-
// biome-ignore lint/suspicious/noExplicitAny: child text
|
|
3222
|
-
// Strip quotes — child is a string literal node,
|
|
3223
|
-
// text includes the surrounding "...".
|
|
3224
|
-
dictKeys.push((child.children[0].text ?? "").replace(/^["']|["']$/g, ""));
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
const missing = namedKeys.filter((k) => !dictKeys.includes(k));
|
|
3228
|
-
return missing.length > 0;
|
|
3229
|
-
}
|
|
3230
|
-
// Format uses named but RHS isn't a dict — definitely wrong
|
|
3231
|
-
return true;
|
|
3232
|
-
}
|
|
3233
|
-
// Positional: count tuple args
|
|
3234
|
-
if (argsNode.type === "tuple") {
|
|
3235
|
-
const argCount = (argsNode.children ?? []).filter((c) => c.isNamed).length;
|
|
3236
|
-
if (argCount !== placeholderCount)
|
|
3237
|
-
return true;
|
|
3238
|
-
}
|
|
3239
|
-
return false;
|
|
3240
|
-
}
|
|
3241
|
-
case "aws_policy_public": {
|
|
3242
|
-
const policyNode = captures.POLICY;
|
|
3243
|
-
if (!policyNode)
|
|
3244
|
-
return false;
|
|
3245
|
-
const text = policyNode.text ?? "";
|
|
3246
|
-
// Match patterns indicating public access
|
|
3247
|
-
const patterns = [
|
|
3248
|
-
/"Principal"\s*:\s*"\*"/, // direct wildcard
|
|
3249
|
-
/"Principal"\s*:\s*\{\s*"AWS"\s*:\s*"\*"\s*\}/, // AWS wildcard
|
|
3250
|
-
/"Effect"\s*:\s*"Allow"[\s\S]*?"Action"\s*:\s*"\*"[\s\S]*?"Resource"\s*:\s*"\*"/, // full admin
|
|
3251
|
-
/"Principal"\s*:\s*"\*"/,
|
|
3252
|
-
];
|
|
3253
|
-
return patterns.some((p) => p.test(text));
|
|
3254
|
-
}
|
|
3255
|
-
case "slots_attribute_mismatch": {
|
|
3256
|
-
const selfNode = captures.SELF;
|
|
3257
|
-
const attrNode = captures.ATTR;
|
|
3258
|
-
const methodNode = captures.METHOD;
|
|
3259
|
-
if (!selfNode || !attrNode || !methodNode)
|
|
3260
|
-
return false;
|
|
3261
|
-
// Only consider self.X = (not other.X)
|
|
3262
|
-
if (selfNode.text !== "self")
|
|
3263
|
-
return false;
|
|
3264
|
-
const attrName = attrNode.text ?? "";
|
|
3265
|
-
// Find parent class_definition
|
|
3266
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST navigation
|
|
3267
|
-
let parent = methodNode.parent;
|
|
3268
|
-
while (parent && parent.type !== "class_definition") {
|
|
3269
|
-
parent = parent.parent;
|
|
3270
|
-
}
|
|
3271
|
-
if (!parent)
|
|
3272
|
-
return false;
|
|
3273
|
-
// Parse the class's __slots__ list and check if attrName is in it.
|
|
3274
|
-
// Fires ONLY when self.X = ... assigns to an attribute NOT in __slots__
|
|
3275
|
-
// (a real S8494 violation — the assignment will raise AttributeError).
|
|
3276
|
-
const slots = extractSlots(parent);
|
|
3277
|
-
// null = no __slots__ declared in this class. [] = __slots__ declared
|
|
3278
|
-
// but we couldn't parse it (treat as null to avoid FPs on inner-class
|
|
3279
|
-
// parent walks where the parent text mentions __slots__ but the
|
|
3280
|
-
// direct children don't contain the assignment).
|
|
3281
|
-
if (slots === null || slots.length === 0)
|
|
3282
|
-
return false;
|
|
3283
|
-
return !slots.includes(attrName);
|
|
3284
|
-
}
|
|
3285
|
-
case "special_method_arity": {
|
|
3286
|
-
const nameNode = captures.NAME;
|
|
3287
|
-
const paramsNode = captures.PARAMS;
|
|
3288
|
-
if (!nameNode || !paramsNode)
|
|
3289
|
-
return false;
|
|
3290
|
-
const name = nameNode.text ?? "";
|
|
3291
|
-
// Expected arities: {method_name: expected_arg_count}
|
|
3292
|
-
// (excluding `self`/`cls` which is always 1)
|
|
3293
|
-
const expected = {
|
|
3294
|
-
__del__: 0,
|
|
3295
|
-
__repr__: 0,
|
|
3296
|
-
__str__: 0,
|
|
3297
|
-
__hash__: 0,
|
|
3298
|
-
__bool__: 0,
|
|
3299
|
-
__len__: 0,
|
|
3300
|
-
__eq__: 1,
|
|
3301
|
-
__lt__: 1,
|
|
3302
|
-
__le__: 1,
|
|
3303
|
-
__gt__: 1,
|
|
3304
|
-
__ge__: 1,
|
|
3305
|
-
__ne__: 1,
|
|
3306
|
-
};
|
|
3307
|
-
const expectedCount = expected[name];
|
|
3308
|
-
if (expectedCount === undefined)
|
|
3309
|
-
return false; // not in our list
|
|
3310
|
-
// Count required params (excluding defaults)
|
|
3311
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST iteration
|
|
3312
|
-
const paramCount = (paramsNode.children ?? []).filter((c) => {
|
|
3313
|
-
if (c.type !== "identifier" && c.type !== "typed_parameter")
|
|
3314
|
-
return false;
|
|
3315
|
-
if (c.text.includes("="))
|
|
3316
|
-
return false;
|
|
3317
|
-
return true;
|
|
3318
|
-
}).length;
|
|
3319
|
-
// Expected total = expectedCount + 1 (for self/cls)
|
|
3320
|
-
return paramCount !== expectedCount + 1;
|
|
3321
|
-
}
|
|
3322
|
-
case "no_super_call": {
|
|
3323
|
-
const bodyNode = captures.BODY;
|
|
3324
|
-
if (!bodyNode)
|
|
3325
|
-
return true;
|
|
3326
|
-
return !/(?<!\/\/.*)super\s*\(/.test(bodyNode.text);
|
|
3327
|
-
}
|
|
3328
|
-
case "in_test_block": {
|
|
3329
|
-
const first = Object.values(captures)[0];
|
|
3330
|
-
return !!first && this.navigator.isInTestBlock(first);
|
|
3331
|
-
}
|
|
3332
|
-
case "not_in_test_block": {
|
|
3333
|
-
const first = Object.values(captures)[0];
|
|
3334
|
-
return !first || !this.navigator.isInTestBlock(first);
|
|
3335
|
-
}
|
|
3336
|
-
case "not_in_try_catch": {
|
|
3337
|
-
const first = Object.values(captures)[0];
|
|
3338
|
-
return !first || !this.navigator.isInTryCatch(first);
|
|
3339
|
-
}
|
|
3340
|
-
case "in_try_catch": {
|
|
3341
|
-
const first = Object.values(captures)[0];
|
|
3342
|
-
return !!first && this.navigator.isInTryCatch(first);
|
|
3343
|
-
}
|
|
3344
|
-
case "name_matches_param": {
|
|
3345
|
-
const nameNode = captures.NAME;
|
|
3346
|
-
const paramNode = captures.PARAM;
|
|
3347
|
-
return !!nameNode && !!paramNode && nameNode.text === paramNode.text;
|
|
3348
|
-
}
|
|
3349
|
-
case "not_in_function": {
|
|
3350
|
-
const first = Object.values(captures)[0];
|
|
3351
|
-
return (!first ||
|
|
3352
|
-
!this.navigator.isInside(first, [
|
|
3353
|
-
"function_definition",
|
|
3354
|
-
"function_declaration",
|
|
3355
|
-
"method_definition",
|
|
3356
|
-
"arrow_function",
|
|
3357
|
-
]));
|
|
3358
|
-
}
|
|
3359
|
-
case "check_secret_pattern": {
|
|
3360
|
-
const varName = captures.VARNAME?.text ?? "";
|
|
3361
|
-
const varNameLower = varName.toLowerCase();
|
|
3362
|
-
// Skip UPPER_CASE constants — they're module-level constants
|
|
3363
|
-
// (e.g. `GITHUB_TYPE_FOR_PERSONAL_API_KEY = "..."`), not secrets.
|
|
3364
|
-
// A constant has no lowercase letters in its name.
|
|
3365
|
-
if (varName === varName.toUpperCase() && /[A-Z]/.test(varName)) {
|
|
3366
|
-
return false;
|
|
3367
|
-
}
|
|
3368
|
-
return [
|
|
3369
|
-
/api[_-]?key/,
|
|
3370
|
-
/api[_-]?secret/,
|
|
3371
|
-
/password/,
|
|
3372
|
-
/passwd/,
|
|
3373
|
-
/secret/,
|
|
3374
|
-
/token/,
|
|
3375
|
-
/auth/,
|
|
3376
|
-
/private[_-]?key/,
|
|
3377
|
-
/access[_-]?token/,
|
|
3378
|
-
/credentials/,
|
|
3379
|
-
/aws[_-]?secret/,
|
|
3380
|
-
/github[_-]?token/,
|
|
3381
|
-
/client[_-]?secret/,
|
|
3382
|
-
].some((p) => p.test(varNameLower));
|
|
3383
|
-
}
|
|
3384
|
-
case "returns_error": {
|
|
3385
|
-
const first = Object.values(captures)[0];
|
|
3386
|
-
if (!first)
|
|
3387
|
-
return false;
|
|
3388
|
-
const funcNode = this.navigator.findParent(first, [
|
|
3389
|
-
"function_declaration",
|
|
3390
|
-
"method_declaration",
|
|
3391
|
-
]);
|
|
3392
|
-
if (!funcNode)
|
|
3393
|
-
return false;
|
|
3394
|
-
const signature = String(funcNode.text ?? "")
|
|
3395
|
-
.split("{", 1)[0]
|
|
3396
|
-
?.trim() ?? "";
|
|
3397
|
-
const returnPart = signature
|
|
3398
|
-
.match(/func\s*(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(.*)$/s)?.[1]
|
|
3399
|
-
?.trim() ?? "";
|
|
3400
|
-
return returnPart.length > 0 && /\berror\b/.test(returnPart);
|
|
3401
|
-
}
|
|
3402
|
-
case "python_empty_except": {
|
|
3403
|
-
const bodyNode = captures.BODY;
|
|
3404
|
-
if (!bodyNode)
|
|
3405
|
-
return true;
|
|
3406
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
|
|
3407
|
-
return !bodyNode.children.some((c) => c.isNamed && c.type !== "pass_statement" && c.type !== "comment");
|
|
3408
|
-
}
|
|
3409
|
-
case "check_in_operator_types": {
|
|
3410
|
-
// `in`/`not in` require __contains__, __iter__ or __getitem__. We can't
|
|
3411
|
-
// do real type inference from a bare identifier, so only flag when the
|
|
3412
|
-
// right-hand side is a literal of a type known NOT to support
|
|
3413
|
-
// containment (None, bool, int, float) — identifiers, strings, lists,
|
|
3414
|
-
// dicts, sets, tuples etc. are left alone to avoid false positives.
|
|
3415
|
-
const target = captures.TARGET;
|
|
3416
|
-
if (!target)
|
|
3417
|
-
return false;
|
|
3418
|
-
return ["none", "true", "false", "integer", "float"].includes(target.type);
|
|
3419
|
-
}
|
|
3420
|
-
case "torchscript_super_call": {
|
|
3421
|
-
// The query only anchors on the `super()` call itself (so it can find
|
|
3422
|
-
// it at any nesting depth inside the method body); this filter walks
|
|
3423
|
-
// back up to confirm the enclosing method OR its class actually carries
|
|
3424
|
-
// a @torch.jit.script / @jit.script decorator.
|
|
3425
|
-
const call = captures.CALL;
|
|
3426
|
-
if (!call)
|
|
3427
|
-
return false;
|
|
3428
|
-
const isTorchScriptDecorated = (node) => {
|
|
3429
|
-
const decorated = node?.parent;
|
|
3430
|
-
if (!decorated || decorated.type !== "decorated_definition")
|
|
3431
|
-
return false;
|
|
3432
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
|
|
3433
|
-
return (decorated.children ?? []).some((c) => c.type === "decorator" &&
|
|
3434
|
-
/^@(torch\.jit\.script|jit\.script)$/.test(c.text ?? ""));
|
|
3435
|
-
};
|
|
3436
|
-
const methodNode = this.navigator.findParent(call, [
|
|
3437
|
-
"function_definition",
|
|
3438
|
-
]);
|
|
3439
|
-
if (!methodNode)
|
|
3440
|
-
return false;
|
|
3441
|
-
const classNode = this.navigator.findParent(methodNode, [
|
|
3442
|
-
"class_definition",
|
|
3443
|
-
]);
|
|
3444
|
-
return (isTorchScriptDecorated(methodNode) ||
|
|
3445
|
-
isTorchScriptDecorated(classNode));
|
|
3446
|
-
}
|
|
3447
|
-
case "exit_params_insufficient": {
|
|
3448
|
-
// __exit__ must accept (self, exc_type, exc_value, traceback) — 4 named
|
|
3449
|
-
// parameters total. The query captures the whole `parameters` node
|
|
3450
|
-
// rather than binding each slot individually: tree-sitter's optional
|
|
3451
|
-
// (`?`) quantifiers on consecutive anchored siblings match every valid
|
|
3452
|
-
// sub-alignment (e.g. self+exc_type alone, or self+exc_type+exc_value),
|
|
3453
|
-
// not just the maximal one, so per-slot captures produce spurious
|
|
3454
|
-
// duplicate/partial matches for a single, fully-correct signature.
|
|
3455
|
-
// Counting named children of the whole node sidesteps that entirely.
|
|
3456
|
-
const params = captures.PARAMS;
|
|
3457
|
-
if (!params)
|
|
3458
|
-
return true;
|
|
3459
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
|
|
3460
|
-
const named = (params.children ?? []).filter((c) => c.isNamed);
|
|
3461
|
-
// `def __exit__(self, *args)` / `(self, *exc_info)` are valid: the splat
|
|
3462
|
-
// absorbs the whole exception triple, so it satisfies every remaining
|
|
3463
|
-
// slot no matter how few named children the node has.
|
|
3464
|
-
if (named.some((c) => c.type === "list_splat_pattern" ||
|
|
3465
|
-
c.type === "dictionary_splat_pattern")) {
|
|
3466
|
-
return false;
|
|
3467
|
-
}
|
|
3468
|
-
return named.length < 4;
|
|
3469
|
-
}
|
|
3470
|
-
case "ruby_empty_rescue": {
|
|
3471
|
-
const bodyNode = captures.BODY;
|
|
3472
|
-
if (!bodyNode)
|
|
3473
|
-
return true;
|
|
3474
|
-
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
|
|
3475
|
-
return !bodyNode.children.some((c) => c.isNamed && !["comment", "nil", "nil_literal"].includes(c.type));
|
|
3476
|
-
}
|
|
3477
|
-
case "ts_command_injection_sink":
|
|
3478
|
-
return (captures.MOD?.text === "child_process" &&
|
|
3479
|
-
/^(exec|execSync)$/.test(captures.FN?.text ?? ""));
|
|
3480
|
-
case "ts_sql_injection_sink": {
|
|
3481
|
-
const template = captures.TEMPLATE?.text ?? "";
|
|
3482
|
-
// Inspect only the template's raw prefix. Leading SQL comments are
|
|
3483
|
-
// ignored; comments and unrelated strings elsewhere cannot satisfy it.
|
|
3484
|
-
const rawPrefix = template
|
|
3485
|
-
.replace(/^`/, "")
|
|
3486
|
-
.replace(/`$/, "")
|
|
3487
|
-
.replace(/^(?:\s|\/\*[\s\S]*?\*\/|--[^\n]*(?:\n|$))*/, "");
|
|
3488
|
-
if (/^(?:SELECT[\s\S]*(?:\bFROM\b|\$\{)|INSERT\s+INTO\b|UPDATE\s+[\s\S]*\bSET\b|DELETE\s+FROM\b|CREATE\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|DROP\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|ALTER\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|GRANT\b|REVOKE\b|WITH[\s\S]*\bSELECT\b|MERGE\s+INTO\b|TRUNCATE\s+TABLE\b|REPLACE\s+INTO\b)/i.test(rawPrefix))
|
|
3489
|
-
return true;
|
|
3490
|
-
if (!rootNode || !captures.OBJ)
|
|
3491
|
-
return false;
|
|
3492
|
-
let imported = TYPESCRIPT_SQL_IMPORTS.get(rootNode);
|
|
3493
|
-
if (!imported) {
|
|
3494
|
-
const names = new Set();
|
|
3495
|
-
const stack = [rootNode];
|
|
3496
|
-
while (stack.length > 0) {
|
|
3497
|
-
const node = stack.pop();
|
|
3498
|
-
if (!node)
|
|
3499
|
-
continue;
|
|
3500
|
-
if (node.type === "import_statement") {
|
|
3501
|
-
const source = node.childForFieldName?.("source")?.text ?? "";
|
|
3502
|
-
const packageName = source.replace(/^['"]|['"]$/g, "");
|
|
3503
|
-
const isKnown = [...TYPESCRIPT_SQL_KNOWN_PACKAGES].some((pkg) => packageName === pkg || packageName.startsWith(`${pkg}/`));
|
|
3504
|
-
if (isKnown) {
|
|
3505
|
-
const importStack = [node];
|
|
3506
|
-
while (importStack.length > 0) {
|
|
3507
|
-
const importNode = importStack.pop();
|
|
3508
|
-
if (!importNode)
|
|
3509
|
-
continue;
|
|
3510
|
-
if (importNode.type === "identifier" ||
|
|
3511
|
-
importNode.type === "import_specifier") {
|
|
3512
|
-
names.add(importNode.text);
|
|
3513
|
-
}
|
|
3514
|
-
importStack.push(...importNode.children);
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
stack.push(...node.children);
|
|
3519
|
-
}
|
|
3520
|
-
const declarations = new Map();
|
|
3521
|
-
const declarationStack = [rootNode];
|
|
3522
|
-
while (declarationStack.length > 0) {
|
|
3523
|
-
const node = declarationStack.pop();
|
|
3524
|
-
if (!node)
|
|
3525
|
-
continue;
|
|
3526
|
-
if (node.type === "variable_declarator") {
|
|
3527
|
-
const name = node.childForFieldName?.("name")?.text;
|
|
3528
|
-
const value = node.childForFieldName?.("value");
|
|
3529
|
-
if (name && value)
|
|
3530
|
-
declarations.set(name, value);
|
|
3531
|
-
}
|
|
3532
|
-
declarationStack.push(...node.children);
|
|
3533
|
-
}
|
|
3534
|
-
const resolvesKnownValue = (node, seen = new Set()) => {
|
|
3535
|
-
if (node.type === "identifier") {
|
|
3536
|
-
if (names.has(node.text))
|
|
3537
|
-
return true;
|
|
3538
|
-
if (seen.has(node.text))
|
|
3539
|
-
return false;
|
|
3540
|
-
const value = declarations.get(node.text);
|
|
3541
|
-
if (!value)
|
|
3542
|
-
return false;
|
|
3543
|
-
seen.add(node.text);
|
|
3544
|
-
return resolvesKnownValue(value, seen);
|
|
3545
|
-
}
|
|
3546
|
-
if (node.type === "await_expression") {
|
|
3547
|
-
const value = node.children.find((child) => child.isNamed);
|
|
3548
|
-
return value ? resolvesKnownValue(value, seen) : false;
|
|
3549
|
-
}
|
|
3550
|
-
if (node.type === "new_expression") {
|
|
3551
|
-
const ctor = node.childForFieldName?.("constructor");
|
|
3552
|
-
return ctor ? resolvesKnownValue(ctor, seen) : false;
|
|
3553
|
-
}
|
|
3554
|
-
if (node.type === "call_expression") {
|
|
3555
|
-
const fn = node.childForFieldName?.("function");
|
|
3556
|
-
return fn ? resolvesKnownValue(fn, seen) : false;
|
|
3557
|
-
}
|
|
3558
|
-
return false;
|
|
3559
|
-
};
|
|
3560
|
-
let changed = true;
|
|
3561
|
-
while (changed) {
|
|
3562
|
-
changed = false;
|
|
3563
|
-
for (const [name, value] of declarations) {
|
|
3564
|
-
if (!names.has(name) && resolvesKnownValue(value)) {
|
|
3565
|
-
names.add(name);
|
|
3566
|
-
changed = true;
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
}
|
|
3570
|
-
imported = names;
|
|
3571
|
-
TYPESCRIPT_SQL_IMPORTS.set(rootNode, imported);
|
|
3572
|
-
}
|
|
3573
|
-
const resolvesToKnownClient = (node) => {
|
|
3574
|
-
if (node.type === "identifier")
|
|
3575
|
-
return imported.has(node.text);
|
|
3576
|
-
if (node.type === "await_expression") {
|
|
3577
|
-
const value = node.children.find((child) => child.isNamed);
|
|
3578
|
-
return value ? resolvesToKnownClient(value) : false;
|
|
3579
|
-
}
|
|
3580
|
-
if (node.type === "new_expression") {
|
|
3581
|
-
const ctor = node.childForFieldName?.("constructor");
|
|
3582
|
-
return ctor ? resolvesToKnownClient(ctor) : false;
|
|
3583
|
-
}
|
|
3584
|
-
if (node.type === "call_expression") {
|
|
3585
|
-
const fn = node.childForFieldName?.("function");
|
|
3586
|
-
return fn ? resolvesToKnownClient(fn) : false;
|
|
3587
|
-
}
|
|
3588
|
-
return false;
|
|
3589
|
-
};
|
|
3590
|
-
return resolvesToKnownClient(captures.OBJ);
|
|
3591
|
-
}
|
|
3592
|
-
case "ts_ssrf_sink": {
|
|
3593
|
-
const fn = captures.FN?.text ?? "";
|
|
3594
|
-
const obj = captures.OBJ?.text ?? "";
|
|
3595
|
-
const urlText = captures.URL?.text ?? "";
|
|
3596
|
-
const allowedFns = new Set([
|
|
3597
|
-
"fetch",
|
|
3598
|
-
"request",
|
|
3599
|
-
"get",
|
|
3600
|
-
"post",
|
|
3601
|
-
"put",
|
|
3602
|
-
"patch",
|
|
3603
|
-
"delete",
|
|
3604
|
-
]);
|
|
3605
|
-
if (!allowedFns.has(fn))
|
|
3606
|
-
return false;
|
|
3607
|
-
// A bare identifier that provably resolves (in this file) to a
|
|
3608
|
-
// `const` initialized with a fixed string/template literal is a
|
|
3609
|
-
// genuinely fixed URL — exempt it regardless of naming
|
|
3610
|
-
// convention (SCREAMING_SNAKE_CASE proves nothing about
|
|
3611
|
-
// provenance; see #963). Anything we cannot definitively prove
|
|
3612
|
-
// falls through to the existing broad heuristic below, so
|
|
3613
|
-
// member expressions, non-literal consts, reassigned
|
|
3614
|
-
// bindings, and unresolved identifiers all keep being
|
|
3615
|
-
// evaluated for taint as before — a false positive here is
|
|
3616
|
-
// acceptable, a missed SSRF is not.
|
|
3617
|
-
if (rootNode &&
|
|
3618
|
-
captures.URL?.type === "identifier" &&
|
|
3619
|
-
this.resolvesToFileLiteralConst(urlText, rootNode)) {
|
|
3620
|
-
return false;
|
|
3621
|
-
}
|
|
3622
|
-
// `fetch(u.toString())` / `fetch(u.href)` where `u` is a file-local
|
|
3623
|
-
// `const u = new URL(<literalPath>, <fixedBase>)`. The origin+path are
|
|
3624
|
-
// fully fixed; dynamic query params set later via
|
|
3625
|
-
// `u.searchParams.set(...)` don't control the destination. Anything we
|
|
3626
|
-
// cannot prove fixed (param/env/member/tainted base) falls through to
|
|
3627
|
-
// the broad heuristic and still fires. See #1000.
|
|
3628
|
-
if (rootNode && captures.URL) {
|
|
3629
|
-
const urlVar = this.newUrlBaseVarName(captures.URL);
|
|
3630
|
-
if (urlVar && this.resolvesToFixedNewUrlConst(urlVar, rootNode)) {
|
|
3631
|
-
return false;
|
|
3632
|
-
}
|
|
3633
|
-
}
|
|
3634
|
-
// Only flag when the URL argument looks like it could carry external
|
|
3635
|
-
// input: member expressions (req.url, ctx.query.x) or identifiers
|
|
3636
|
-
// whose names suggest user/external provenance. Plain generic names
|
|
3637
|
-
// like `url` or `path` in internal download utilities produce too
|
|
3638
|
-
// many false positives — those need data-flow analysis to resolve.
|
|
3639
|
-
const looksLikeExternalInput = urlText.includes(".") ||
|
|
3640
|
-
/user|external|remote|input|target|webhook|callback|redirect|untrusted|arbitrary/i.test(urlText);
|
|
3641
|
-
if (!looksLikeExternalInput)
|
|
3642
|
-
return false;
|
|
3643
|
-
if (!obj)
|
|
3644
|
-
return fn === "fetch";
|
|
3645
|
-
return new Set([
|
|
3646
|
-
"axios",
|
|
3647
|
-
"http",
|
|
3648
|
-
"https",
|
|
3649
|
-
"got",
|
|
3650
|
-
"request",
|
|
3651
|
-
"superagent",
|
|
3652
|
-
"undici",
|
|
3653
|
-
]).has(obj);
|
|
3654
|
-
}
|
|
3655
|
-
case "ts_weak_hash_algorithm":
|
|
3656
|
-
return (captures.FN?.text === "createHash" &&
|
|
3657
|
-
/^(md5|sha1)$/i.test(captures.ALG?.text ?? ""));
|
|
3658
|
-
case "ts_insecure_random_source": {
|
|
3659
|
-
if (captures.OBJ?.text !== "Math" || captures.FN?.text !== "random")
|
|
3660
|
-
return false;
|
|
3661
|
-
// Only flag when the result flows into a security-sensitive binding.
|
|
3662
|
-
// Walk up from the `Math.random()` call so chained forms such as
|
|
3663
|
-
// `Math.random().toString(36)` are still attributed to their binding.
|
|
3664
|
-
const varName = this.enclosingBindingName(captures.CALL ?? captures.VAR);
|
|
3665
|
-
return /token|secret|password|key|nonce|salt|csrf|auth|session|credential|hash|otp|pin/i.test(varName);
|
|
3666
|
-
}
|
|
3667
|
-
case "ts_detached_async_call":
|
|
3668
|
-
return /(Async$|fetch$|request$)/.test(captures.FN?.text ?? "");
|
|
3669
|
-
case "incomplete_assertion": {
|
|
3670
|
-
const expectNode = captures.EXPECT;
|
|
3671
|
-
if (!expectNode)
|
|
3672
|
-
return false;
|
|
3673
|
-
const CHAI_PROPERTY_ASSERTIONS = new Set([
|
|
3674
|
-
"true",
|
|
3675
|
-
"false",
|
|
3676
|
-
"null",
|
|
3677
|
-
"undefined",
|
|
3678
|
-
"empty",
|
|
3679
|
-
"NaN",
|
|
3680
|
-
"finite",
|
|
3681
|
-
"exist",
|
|
3682
|
-
"arguments",
|
|
3683
|
-
"extensible",
|
|
3684
|
-
"sealed",
|
|
3685
|
-
"frozen",
|
|
3686
|
-
"locked",
|
|
3687
|
-
]);
|
|
3688
|
-
// The expect identifier is inside a call_expression. Walk up past that
|
|
3689
|
-
// call_expression to the container that determines if it's a complete
|
|
3690
|
-
// assertion or an incomplete one.
|
|
3691
|
-
let current = expectNode.parent;
|
|
3692
|
-
if (!current)
|
|
3693
|
-
return false;
|
|
3694
|
-
current = current.parent; // skip the expect(...) call_expression
|
|
3695
|
-
if (!current)
|
|
3696
|
-
return false;
|
|
3697
|
-
// Bare expect(foo); or return expect(foo);
|
|
3698
|
-
if (current.type === "expression_statement" ||
|
|
3699
|
-
current.type === "return_statement")
|
|
3700
|
-
return true;
|
|
3701
|
-
let lastPropertyName = null;
|
|
3702
|
-
while (current && current.type === "member_expression") {
|
|
3703
|
-
const propNode = current.children?.find((c) => c.type === "property_identifier");
|
|
3704
|
-
if (propNode)
|
|
3705
|
-
lastPropertyName = propNode.text;
|
|
3706
|
-
const parent = current.parent;
|
|
3707
|
-
if (!parent)
|
|
3708
|
-
return false;
|
|
3709
|
-
if (parent.type === "expression_statement" ||
|
|
3710
|
-
parent.type === "return_statement") {
|
|
3711
|
-
if (lastPropertyName &&
|
|
3712
|
-
CHAI_PROPERTY_ASSERTIONS.has(lastPropertyName))
|
|
3713
|
-
return false;
|
|
3714
|
-
return true;
|
|
3715
|
-
}
|
|
3716
|
-
if (parent.type === "call_expression")
|
|
3717
|
-
return false;
|
|
3718
|
-
current = parent;
|
|
3719
|
-
}
|
|
3720
|
-
return false;
|
|
3721
|
-
}
|
|
3722
|
-
case "py_command_injection_sink": {
|
|
3723
|
-
const mod = captures.MOD?.text ?? "";
|
|
3724
|
-
const fn = captures.FN?.text ?? "";
|
|
3725
|
-
const kw = captures.KW?.text ?? "";
|
|
3726
|
-
return ((mod === "os" && /^(system|popen)$/.test(fn)) ||
|
|
3727
|
-
(mod === "subprocess" &&
|
|
3728
|
-
/^(run|Popen|call|check_output|check_call)$/.test(fn) &&
|
|
3729
|
-
kw === "shell"));
|
|
3730
|
-
}
|
|
3731
|
-
case "go_command_injection_sink":
|
|
3732
|
-
return (captures.PKG?.text === "exec" &&
|
|
3733
|
-
/^(Command|CommandContext)$/.test(captures.FN?.text ?? "") &&
|
|
3734
|
-
/^"(sh|bash|zsh|cmd|powershell|pwsh)"$/.test(captures.SHELL?.text ?? "") &&
|
|
3735
|
-
/^"(-c|\/c)"$/.test(captures.FLAG?.text ?? ""));
|
|
3736
|
-
case "ruby_command_injection_sink":
|
|
3737
|
-
return /^(system|exec|spawn|popen|capture3|capture2|capture2e)$/.test(captures.FN?.text ?? "");
|
|
3738
|
-
case "py_ssrf_sink":
|
|
3739
|
-
return (captures.MOD?.text === "requests" &&
|
|
3740
|
-
/^(get|post|put|patch|delete|request|head|options)$/.test(captures.FN?.text ?? ""));
|
|
3741
|
-
case "py_path_traversal_sink":
|
|
3742
|
-
return /^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$/.test(captures.FN?.text ?? "");
|
|
3743
|
-
case "go_path_traversal_sink":
|
|
3744
|
-
return (/^(os|ioutil)$/.test(captures.PKG?.text ?? "") &&
|
|
3745
|
-
/^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(captures.FN?.text ?? ""));
|
|
3746
|
-
case "py_sql_injection_sink": {
|
|
3747
|
-
const fn = captures.FN?.text ?? "";
|
|
3748
|
-
if (!PYTHON_SQL_SINK_METHODS.has(fn))
|
|
3749
|
-
return false;
|
|
3750
|
-
const sqlNode = captures.SQL;
|
|
3751
|
-
const receiver = captures.OBJ;
|
|
3752
|
-
// Pre-existing exemptions — kept verbatim so no current user
|
|
3753
|
-
// regresses (#2577 review): an unannotated session receiver, and a
|
|
3754
|
-
// statement-builder call in argument position.
|
|
3755
|
-
if (fn === "execute" &&
|
|
3756
|
-
this.isLikelySqlAlchemyReceiver(receiver?.text ?? "")) {
|
|
3757
|
-
return false;
|
|
3758
|
-
}
|
|
3759
|
-
if (sqlNode && this.isSafeSqlAlchemyExpressionCall(sqlNode)) {
|
|
3760
|
-
return false;
|
|
3761
|
-
}
|
|
3762
|
-
// #2576: a receiver PROVEN to be a sqlalchemy Session/AsyncSession.
|
|
3763
|
-
// `Session.query` takes entity classes and `Session.execute` a
|
|
3764
|
-
// statement object (a builder call, or a name bound to one —
|
|
3765
|
-
// `stmt = select(User)`, which the check above cannot see). Neither
|
|
3766
|
-
// suppression may swallow composed SQL text (#2577 review F1/F2).
|
|
3767
|
-
if (isProvenSqlAlchemySessionReceiver(receiver, rootNode)) {
|
|
3768
|
-
if (fn === "query" &&
|
|
3769
|
-
isSqlAlchemyEntityQueryArgument(sqlNode, rootNode)) {
|
|
3770
|
-
return false;
|
|
3771
|
-
}
|
|
3772
|
-
if (fn === "execute" &&
|
|
3773
|
-
isSqlAlchemyStatementArgument(sqlNode, rootNode)) {
|
|
3774
|
-
return false;
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
// #2576: psycopg `sql.SQL("...").format(sql.Identifier(...))`.
|
|
3778
|
-
if (isSafePsycopgIdentifierComposition(sqlNode, rootNode)) {
|
|
3779
|
-
return false;
|
|
3780
|
-
}
|
|
3781
|
-
return true;
|
|
3782
|
-
}
|
|
3783
|
-
case "go_sql_injection_sink":
|
|
3784
|
-
return (/^(Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext)$/.test(captures.DBFN?.text ?? "") &&
|
|
3785
|
-
captures.FMTPKG?.text === "fmt" &&
|
|
3786
|
-
captures.FMTFN?.text === "Sprintf");
|
|
3787
|
-
case "py_insecure_deserialization_sink":
|
|
3788
|
-
return (/^(pickle|yaml)$/.test(captures.MOD?.text ?? "") &&
|
|
3789
|
-
/^(load|loads|unsafe_load)$/.test(captures.FN?.text ?? ""));
|
|
3790
|
-
case "ruby_insecure_deserialization_sink":
|
|
3791
|
-
return (/^(Marshal|YAML|Psych)$/.test(captures.MOD?.text ?? "") &&
|
|
3792
|
-
/^(load|unsafe_load)$/.test(captures.FN?.text ?? ""));
|
|
3793
|
-
case "match_captures": {
|
|
3794
|
-
// Generic filter: each key in postFilterParams is a capture name,
|
|
3795
|
-
// value is a regex string. All must match.
|
|
3796
|
-
for (const [captureName, pattern] of Object.entries(postFilterParams ?? {})) {
|
|
3797
|
-
const node = captures[captureName];
|
|
3798
|
-
if (!node)
|
|
3799
|
-
return false;
|
|
3800
|
-
if (!new RegExp(pattern).test(node.text))
|
|
3801
|
-
return false;
|
|
3802
|
-
}
|
|
3803
|
-
return true;
|
|
3804
|
-
}
|
|
3805
|
-
case "case_range_single_value": {
|
|
3806
|
-
const start = captures.START?.text ?? "";
|
|
3807
|
-
const end = captures.END?.text ?? "";
|
|
3808
|
-
return start === end;
|
|
3809
|
-
}
|
|
3810
|
-
case "goto_jumps_backward": {
|
|
3811
|
-
const label = captures.LABEL;
|
|
3812
|
-
const gotoNode = captures.GOTO;
|
|
3813
|
-
if (!label || !gotoNode)
|
|
3814
|
-
return false;
|
|
3815
|
-
return label.startIndex < gotoNode.startIndex;
|
|
3816
|
-
}
|
|
3817
|
-
case "goto_targets_inner_block": {
|
|
3818
|
-
const target = captures.TARGET;
|
|
3819
|
-
if (!target)
|
|
3820
|
-
return false;
|
|
3821
|
-
// A goto targets an inner block if its label is inside a
|
|
3822
|
-
// compound_statement that is nested inside another compound_statement.
|
|
3823
|
-
let depth = 0;
|
|
3824
|
-
let node = target.parent;
|
|
3825
|
-
while (node) {
|
|
3826
|
-
if (node.type === "compound_statement")
|
|
3827
|
-
depth++;
|
|
3828
|
-
node = node.parent;
|
|
3829
|
-
}
|
|
3830
|
-
return depth >= 2;
|
|
3831
|
-
}
|
|
3832
|
-
case "c_memset_sensitive_arg": {
|
|
3833
|
-
const callNode = captures.CALL;
|
|
3834
|
-
if (!callNode || callNode.type !== "call_expression")
|
|
3835
|
-
return false;
|
|
3836
|
-
// Find the first argument in the argument_list
|
|
3837
|
-
const argList = callNode.children?.find((c) => c.type === "argument_list");
|
|
3838
|
-
if (!argList)
|
|
3839
|
-
return false;
|
|
3840
|
-
// First named child after the opening paren is the first arg
|
|
3841
|
-
const firstArg = argList.children?.find((c) => c.isNamed);
|
|
3842
|
-
if (!firstArg)
|
|
3843
|
-
return false;
|
|
3844
|
-
const argName = firstArg.text ?? "";
|
|
3845
|
-
return /password|secret|key|token|credential|auth|private|passwd|pin|salt|nonce|iv|seed/i.test(argName);
|
|
3846
|
-
}
|
|
3847
|
-
case "c_stdlib_name": {
|
|
3848
|
-
const name = captures.NAME?.text ?? "";
|
|
3849
|
-
const STDLIB_NAMES = new Set([
|
|
3850
|
-
"malloc",
|
|
3851
|
-
"calloc",
|
|
3852
|
-
"realloc",
|
|
3853
|
-
"free",
|
|
3854
|
-
"alloca",
|
|
3855
|
-
"printf",
|
|
3856
|
-
"fprintf",
|
|
3857
|
-
"sprintf",
|
|
3858
|
-
"snprintf",
|
|
3859
|
-
"vprintf",
|
|
3860
|
-
"vfprintf",
|
|
3861
|
-
"vsprintf",
|
|
3862
|
-
"vsnprintf",
|
|
3863
|
-
"scanf",
|
|
3864
|
-
"fscanf",
|
|
3865
|
-
"sscanf",
|
|
3866
|
-
"vscanf",
|
|
3867
|
-
"vfscanf",
|
|
3868
|
-
"vsscanf",
|
|
3869
|
-
"strcpy",
|
|
3870
|
-
"strncpy",
|
|
3871
|
-
"strcat",
|
|
3872
|
-
"strncat",
|
|
3873
|
-
"strcmp",
|
|
3874
|
-
"strncmp",
|
|
3875
|
-
"strlen",
|
|
3876
|
-
"strchr",
|
|
3877
|
-
"strrchr",
|
|
3878
|
-
"strstr",
|
|
3879
|
-
"strerror",
|
|
3880
|
-
"memcpy",
|
|
3881
|
-
"memmove",
|
|
3882
|
-
"memset",
|
|
3883
|
-
"memcmp",
|
|
3884
|
-
"memchr",
|
|
3885
|
-
"fopen",
|
|
3886
|
-
"fclose",
|
|
3887
|
-
"fread",
|
|
3888
|
-
"fwrite",
|
|
3889
|
-
"fgets",
|
|
3890
|
-
"fputs",
|
|
3891
|
-
"getc",
|
|
3892
|
-
"putc",
|
|
3893
|
-
"getchar",
|
|
3894
|
-
"putchar",
|
|
3895
|
-
"exit",
|
|
3896
|
-
"abort",
|
|
3897
|
-
"assert",
|
|
3898
|
-
"errno",
|
|
3899
|
-
"abs",
|
|
3900
|
-
"labs",
|
|
3901
|
-
"llabs",
|
|
3902
|
-
"div",
|
|
3903
|
-
"ldiv",
|
|
3904
|
-
"lldiv",
|
|
3905
|
-
"atoi",
|
|
3906
|
-
"atol",
|
|
3907
|
-
"atoll",
|
|
3908
|
-
"strtol",
|
|
3909
|
-
"strtoll",
|
|
3910
|
-
"strtoul",
|
|
3911
|
-
"strtoull",
|
|
3912
|
-
"strtod",
|
|
3913
|
-
"strtof",
|
|
3914
|
-
"strtold",
|
|
3915
|
-
"qsort",
|
|
3916
|
-
"bsearch",
|
|
3917
|
-
"time",
|
|
3918
|
-
"clock",
|
|
3919
|
-
"difftime",
|
|
3920
|
-
"mktime",
|
|
3921
|
-
"strftime",
|
|
3922
|
-
"getenv",
|
|
3923
|
-
"setenv",
|
|
3924
|
-
"putenv",
|
|
3925
|
-
"system",
|
|
3926
|
-
"isalpha",
|
|
3927
|
-
"isdigit",
|
|
3928
|
-
"isalnum",
|
|
3929
|
-
"isspace",
|
|
3930
|
-
"isupper",
|
|
3931
|
-
"islower",
|
|
3932
|
-
"toupper",
|
|
3933
|
-
"tolower",
|
|
3934
|
-
"sizeof",
|
|
3935
|
-
"offsetof",
|
|
3936
|
-
"NULL",
|
|
3937
|
-
"EXIT_SUCCESS",
|
|
3938
|
-
"EXIT_FAILURE",
|
|
3939
|
-
]);
|
|
3940
|
-
return STDLIB_NAMES.has(name);
|
|
3941
|
-
}
|
|
3942
|
-
case "c_octal_literal": {
|
|
3943
|
-
const num = captures.NUM?.text ?? "";
|
|
3944
|
-
return /^0[0-7]+$/.test(num);
|
|
3945
|
-
}
|
|
3946
|
-
case "c_noreturn_attr": {
|
|
3947
|
-
const attr = captures.ATTR?.text ?? "";
|
|
3948
|
-
return attr === "noreturn";
|
|
3949
|
-
}
|
|
3950
|
-
case "c_label_in_switch": {
|
|
3951
|
-
const stmt = captures.STMT;
|
|
3952
|
-
if (!stmt)
|
|
3953
|
-
return false;
|
|
3954
|
-
let node = stmt.parent;
|
|
3955
|
-
while (node) {
|
|
3956
|
-
if (node.type === "switch_statement")
|
|
3957
|
-
return true;
|
|
3958
|
-
node = node.parent;
|
|
3959
|
-
}
|
|
3960
|
-
return false;
|
|
3961
|
-
}
|
|
3962
|
-
default:
|
|
3963
|
-
// A rule whose post_filter has no implementation cannot honour its
|
|
3964
|
-
// own definition, so DROP the match instead of reporting every raw
|
|
3965
|
-
// structural hit. Failing open here made `duplicate-function-arg`
|
|
3966
|
-
// (`same_param_name`, unimplemented) fire on 59 of 60 files that
|
|
3967
|
-
// contain no duplicate parameter at all.
|
|
3968
|
-
this.reportMissingPostFilter(postFilter);
|
|
3969
|
-
return false;
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
reportedMissingPostFilters = new Set();
|
|
3973
|
-
reportedPostFilterFailures = new Set();
|
|
3974
|
-
/** Keep a failed post-filter match and leave a bounded diagnostic trail. */
|
|
3975
|
-
reportPostFilterFailure(postFilter, reason) {
|
|
3976
|
-
if (this.reportedPostFilterFailures.has(postFilter))
|
|
3977
|
-
return;
|
|
3978
|
-
this.reportedPostFilterFailures.add(postFilter);
|
|
3979
|
-
logTreeSitterDiagnostic({
|
|
3980
|
-
subsystem: "tree-sitter-client",
|
|
3981
|
-
message: `tree-sitter rule post_filter '${postFilter}' failed — ` +
|
|
3982
|
-
`keeping the diagnostic (fail-open): ${reason}.`,
|
|
3983
|
-
metadata: { postFilter },
|
|
3984
|
-
});
|
|
3985
|
-
}
|
|
3986
|
-
/** Warn once per unimplemented post_filter — a silent rule needs a trail. */
|
|
3987
|
-
reportMissingPostFilter(postFilter) {
|
|
3988
|
-
if (this.reportedMissingPostFilters.has(postFilter))
|
|
3989
|
-
return;
|
|
3990
|
-
this.reportedMissingPostFilters.add(postFilter);
|
|
3991
|
-
logTreeSitterDiagnostic({
|
|
3992
|
-
subsystem: "tree-sitter-client",
|
|
3993
|
-
message: `tree-sitter rule post_filter '${postFilter}' is not implemented — ` +
|
|
3994
|
-
`matches for the rules using it are suppressed rather than reported unfiltered. ` +
|
|
3995
|
-
`Implement it in applyPostFilter (clients/tree-sitter-client.ts) to re-enable them.`,
|
|
3996
|
-
metadata: { postFilter },
|
|
3997
|
-
});
|
|
3998
|
-
}
|
|
3999
|
-
/**
|
|
4000
|
-
* Latch so the "textPredicates is malformed" diagnostic log line fires once
|
|
4001
|
-
* per `TreeSitterClient` instance for the life of the process (this client is
|
|
4002
|
-
* a process-wide singleton in production — see clients/tree-sitter-shared.ts),
|
|
4003
|
-
* not once per call. This gates ONLY the log line: process-wide log volume is
|
|
4004
|
-
* the concern here, not the user-facing degradation record below, which must
|
|
4005
|
-
* re-arm every session (see recordDegradationOnce) — including for a Query
|
|
4006
|
-
* object the LRU query cache (queryCache/queryBatchCache above) kept alive
|
|
4007
|
-
* across a session boundary, since the cache is evicted, never cleared.
|
|
4008
|
-
*/
|
|
4009
|
-
textPredicatesInvalidLogged = false;
|
|
4010
|
-
/**
|
|
4011
|
-
* Typed accessor for `query.textPredicates`. If the property is missing or the
|
|
4012
|
-
* wrong shape — e.g. a future web-tree-sitter upgrade renames or removes it —
|
|
4013
|
-
* `query.textPredicates?.[i] ?? []` would silently mean "no predicates for this
|
|
4014
|
-
* pattern," passing every match through unfiltered. That's a silent fail-open on
|
|
4015
|
-
* #match?/#eq? predicates that rules rely on for correctness, and because this
|
|
4016
|
-
* runs for every match of every query, it would zero out ALL structural matches
|
|
4017
|
-
* across every language while pi-lens reports "no issues found." Fail loud (log
|
|
4018
|
-
* once per client instance, record a degradation so it reaches the user) and
|
|
4019
|
-
* closed (report the query has no usable predicates) instead of guessing.
|
|
4020
|
-
*
|
|
4021
|
-
* No WeakSet short-circuit for already-known-bad queries here (#1523 review
|
|
4022
|
-
* R1): `Array.isArray` is O(1), so memoizing it buys nothing, and a
|
|
4023
|
-
* process-lifetime WeakSet would make the SAME cached Query object (the query
|
|
4024
|
-
* cache is LRU-evicted, never cleared) skip straight past
|
|
4025
|
-
* `recordDegradationOnce` on every call after the first — which is exactly
|
|
4026
|
-
* what made the degradation record fail to re-arm across a session boundary
|
|
4027
|
-
* for a query the cache kept warm. `recordDegradationOnce` itself is called
|
|
4028
|
-
* unconditionally on every invalid check; the ledger's own once-per-kind/
|
|
4029
|
-
* subject dedupe (cleared by resetDegradationLedger, which handleSessionStart
|
|
4030
|
-
* calls first thing — clients/runtime-session.ts) is the single source of
|
|
4031
|
-
* truth for "how often does this actually get recorded."
|
|
4032
|
-
*/
|
|
4033
|
-
// biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter Query instances
|
|
4034
|
-
hasValidTextPredicates(query) {
|
|
4035
|
-
const valid = Array.isArray(query?.textPredicates);
|
|
4036
|
-
if (!valid) {
|
|
4037
|
-
if (!this.textPredicatesInvalidLogged) {
|
|
4038
|
-
this.textPredicatesInvalidLogged = true;
|
|
4039
|
-
logTreeSitterDiagnostic({
|
|
4040
|
-
subsystem: "tree-sitter-client",
|
|
4041
|
-
message: "web-tree-sitter Query.textPredicates is missing or not an array — " +
|
|
4042
|
-
"#match?/#eq? predicates cannot be evaluated. Failing CLOSED: matches " +
|
|
4043
|
-
"for this query are dropped rather than reported unfiltered.",
|
|
4044
|
-
metadata: { textPredicatesType: typeof query?.textPredicates },
|
|
4045
|
-
});
|
|
4046
|
-
}
|
|
4047
|
-
// User-facing signal (#1523 review F1). Called unconditionally on every
|
|
4048
|
-
// invalid check (not gated by a client-lifetime latch) so it re-arms
|
|
4049
|
-
// every session, per the R1 fix above.
|
|
4050
|
-
recordDegradationOnce({
|
|
4051
|
-
kind: "query-predicates-invalid",
|
|
4052
|
-
subject: "web-tree-sitter",
|
|
4053
|
-
reason: "Query.textPredicates is missing or not an array — #match?/#eq? " +
|
|
4054
|
-
"predicates cannot be evaluated; structural matches relying on them " +
|
|
4055
|
-
"are dropped fail-closed",
|
|
4056
|
-
});
|
|
4057
|
-
}
|
|
4058
|
-
return valid;
|
|
4059
|
-
}
|
|
4060
|
-
/**
|
|
4061
|
-
* Evaluate text predicates (#match?, #eq?) for a query match. web-tree-sitter
|
|
4062
|
-
* 0.25's `Query.matches()` DOES apply these predicates itself (probed on the
|
|
4063
|
-
* shipped grammars — see clients/tree-sitter-symbol-extractor.ts), so this is a
|
|
4064
|
-
* belt-and-braces re-filter, not a workaround for missing enforcement. It also
|
|
4065
|
-
* doubles as the fail-closed guard (#1523): if `query.textPredicates` is ever
|
|
4066
|
-
* missing or malformed, this drops the match instead of assuming it already
|
|
4067
|
-
* passed upstream.
|
|
4068
|
-
*/
|
|
4069
|
-
// biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter types
|
|
4070
|
-
evaluatePredicates(query, match) {
|
|
4071
|
-
if (!this.hasValidTextPredicates(query))
|
|
4072
|
-
return false;
|
|
4073
|
-
// `?.` guards a post-validation removal of the property (e.g. a later
|
|
4074
|
-
// mutation strips it after the first successful check) so the read can't
|
|
4075
|
-
// throw here — an uncaught throw would be swallowed by the outer try/catch
|
|
4076
|
-
// in searchFileWithQuery and silently zero out matches for the whole file.
|
|
4077
|
-
const predicates = query.textPredicates?.[match.patternIndex] ?? [];
|
|
4078
|
-
return predicates.every((fn) => fn(match.captures));
|
|
4079
|
-
}
|
|
4080
|
-
/** Search a single file using tree-sitter Query */
|
|
4081
|
-
async searchFileWithQuery(filePath,
|
|
4082
|
-
// biome-ignore lint/suspicious/noExplicitAny: Query type from web-tree-sitter
|
|
4083
|
-
query, metavars, languageId, _originalPattern, postFilter,
|
|
4084
|
-
// biome-ignore lint/suspicious/noExplicitAny: Post filter params
|
|
4085
|
-
postFilterParams, contentOverride) {
|
|
4086
|
-
const matches = [];
|
|
4087
|
-
await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
|
|
4088
|
-
try {
|
|
4089
|
-
const rootNode = tree.rootNode;
|
|
4090
|
-
const queryMatches = query.matches(rootNode);
|
|
4091
|
-
for (const match of queryMatches) {
|
|
4092
|
-
const captures = {};
|
|
4093
|
-
for (const capture of match.captures) {
|
|
4094
|
-
if (metavars.includes(capture.name)) {
|
|
4095
|
-
captures[capture.name] = capture.node;
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
// Belt-and-braces re-check of #match?/#eq? predicates (web-tree-sitter
|
|
4099
|
-
// 0.25's Query.matches() already applies them) plus the fail-closed
|
|
4100
|
-
// guard for a malformed query.textPredicates (see evaluatePredicates).
|
|
4101
|
-
if (!this.evaluatePredicates(query, match)) {
|
|
4102
|
-
continue;
|
|
4103
|
-
}
|
|
4104
|
-
if (postFilter &&
|
|
4105
|
-
!this.applyPostFilter(postFilter, postFilterParams, captures, rootNode)) {
|
|
4106
|
-
continue;
|
|
4107
|
-
}
|
|
4108
|
-
if (match.captures.length > 0) {
|
|
4109
|
-
const firstNode = match.captures[0].node;
|
|
4110
|
-
const textCaptures = {};
|
|
4111
|
-
for (const [name, node] of Object.entries(captures)) {
|
|
4112
|
-
textCaptures[name] = node.text;
|
|
4113
|
-
}
|
|
4114
|
-
matches.push({
|
|
4115
|
-
file: filePath,
|
|
4116
|
-
line: firstNode.startPosition.row + 1,
|
|
4117
|
-
column: firstNode.startPosition.column + 1,
|
|
4118
|
-
matchedText: firstNode.text,
|
|
4119
|
-
nodeType: firstNode.type,
|
|
4120
|
-
captures: textCaptures,
|
|
4121
|
-
});
|
|
4122
|
-
}
|
|
4123
|
-
}
|
|
4124
|
-
if (matches.length > 0) {
|
|
4125
|
-
this.dbg(`Found ${matches.length} matches in ${path.basename(filePath)}`);
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
|
-
catch (err) {
|
|
4129
|
-
this.reportWasmAbort(err);
|
|
4130
|
-
this.dbg(`Query matching error: ${err}`);
|
|
4131
|
-
}
|
|
4132
|
-
});
|
|
4133
|
-
return matches;
|
|
4134
|
-
}
|
|
4135
|
-
/** Collect source files for a language */
|
|
4136
|
-
collectFiles(dir, languageId, fileFilter) {
|
|
4137
|
-
const files = [];
|
|
4138
|
-
const extensions = this.getExtensionsForLanguage(languageId);
|
|
4139
|
-
const rootDir = path.resolve(dir);
|
|
4140
|
-
const ignoreMatcher = getProjectIgnoreMatcher(rootDir);
|
|
4141
|
-
// Hard cap on the walk itself (not just result collection). The per-file
|
|
4142
|
-
// `maxResults` break upstream only stops gathering matches *after* the walk
|
|
4143
|
-
// has already enumerated the whole tree — so a misrooted structuralSearch
|
|
4144
|
-
// would still synchronously read every directory. Bound the walk (#262).
|
|
4145
|
-
const scan = (d) => {
|
|
4146
|
-
if (files.length >= TREE_SITTER_MAX_SCAN_FILES)
|
|
4147
|
-
return;
|
|
4148
|
-
try {
|
|
4149
|
-
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
4150
|
-
for (const entry of entries) {
|
|
4151
|
-
if (files.length >= TREE_SITTER_MAX_SCAN_FILES)
|
|
4152
|
-
return;
|
|
4153
|
-
const full = path.join(d, entry.name);
|
|
4154
|
-
if (entry.isDirectory()) {
|
|
4155
|
-
if (isExcludedDirName(entry.name))
|
|
4156
|
-
continue;
|
|
4157
|
-
if (ignoreMatcher.isIgnored(full, true))
|
|
4158
|
-
continue;
|
|
4159
|
-
scan(full);
|
|
4160
|
-
}
|
|
4161
|
-
else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
4162
|
-
if (ignoreMatcher.isIgnored(full, false))
|
|
4163
|
-
continue;
|
|
4164
|
-
if (!fileFilter || fileFilter(full)) {
|
|
4165
|
-
files.push(full);
|
|
4166
|
-
}
|
|
4167
|
-
}
|
|
4168
|
-
}
|
|
4169
|
-
}
|
|
4170
|
-
catch { }
|
|
4171
|
-
};
|
|
4172
|
-
scan(rootDir);
|
|
4173
|
-
return files;
|
|
4174
|
-
}
|
|
4175
|
-
/** Get file extensions for a language */
|
|
4176
|
-
getExtensionsForLanguage(languageId) {
|
|
4177
|
-
const mapping = {
|
|
4178
|
-
typescript: [".ts", ".mts", ".cts"],
|
|
4179
|
-
tsx: [".tsx"],
|
|
4180
|
-
javascript: [".js", ".mjs", ".cjs"],
|
|
4181
|
-
python: [".py"],
|
|
4182
|
-
rust: [".rs"],
|
|
4183
|
-
go: [".go"],
|
|
4184
|
-
java: [".java"],
|
|
4185
|
-
kotlin: [".kt", ".kts"],
|
|
4186
|
-
dart: [".dart"],
|
|
4187
|
-
c: [".c", ".h"],
|
|
4188
|
-
cpp: [".cpp", ".hpp", ".cc", ".hh"],
|
|
4189
|
-
elixir: [".ex", ".exs"],
|
|
4190
|
-
ruby: [".rb"],
|
|
4191
|
-
};
|
|
4192
|
-
return mapping[languageId] || [];
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
// --- Simplified Pattern Search (regex fallback) ---
|