@backbencher/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,1515 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FILE_CLAIM_MAX_BYTES,
|
|
3
|
+
dirOf,
|
|
4
|
+
fileClaimForTarget,
|
|
5
|
+
foldProjectPath
|
|
6
|
+
} from "./chunk-EWMWK7WN.js";
|
|
7
|
+
import {
|
|
8
|
+
RealCssAnalyzer,
|
|
9
|
+
cssFileScopeOf
|
|
10
|
+
} from "./chunk-IFZNR5UJ.js";
|
|
11
|
+
import {
|
|
12
|
+
ASTEngine
|
|
13
|
+
} from "./chunk-V44PGPA6.js";
|
|
14
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
15
|
+
import "./chunk-SK6YVHFZ.js";
|
|
16
|
+
import "./chunk-2BSKD7O3.js";
|
|
17
|
+
import "./chunk-KAET47ZL.js";
|
|
18
|
+
import "./chunk-KCDECRJY.js";
|
|
19
|
+
import "./chunk-MLKGABMK.js";
|
|
20
|
+
|
|
21
|
+
// src/ast/real-scss-analyzer.ts
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
import { existsSync } from "fs";
|
|
24
|
+
function stripAllQuotes(text) {
|
|
25
|
+
let out = "";
|
|
26
|
+
for (let i = 0; i < text.length; i++) {
|
|
27
|
+
const c = text[i];
|
|
28
|
+
if (c === '"' || c === "'") {
|
|
29
|
+
const close = text.indexOf(c, i + 1);
|
|
30
|
+
if (close < 0) break;
|
|
31
|
+
out += text.slice(i + 1, close);
|
|
32
|
+
i = close;
|
|
33
|
+
} else {
|
|
34
|
+
out += c;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out.trim();
|
|
38
|
+
}
|
|
39
|
+
var JS_WHITESPACE = /* @__PURE__ */ new Set([
|
|
40
|
+
" ",
|
|
41
|
+
" ",
|
|
42
|
+
"\n",
|
|
43
|
+
"\v",
|
|
44
|
+
"\f",
|
|
45
|
+
"\r",
|
|
46
|
+
"\xA0",
|
|
47
|
+
"\u1680",
|
|
48
|
+
"\u2000",
|
|
49
|
+
"\u2001",
|
|
50
|
+
"\u2002",
|
|
51
|
+
"\u2003",
|
|
52
|
+
"\u2004",
|
|
53
|
+
"\u2005",
|
|
54
|
+
"\u2006",
|
|
55
|
+
"\u2007",
|
|
56
|
+
"\u2008",
|
|
57
|
+
"\u2009",
|
|
58
|
+
"\u200A",
|
|
59
|
+
"\u2028",
|
|
60
|
+
"\u2029",
|
|
61
|
+
"\u202F",
|
|
62
|
+
"\u205F",
|
|
63
|
+
"\u3000",
|
|
64
|
+
"\uFEFF"
|
|
65
|
+
]);
|
|
66
|
+
function isWordCharCode(code) {
|
|
67
|
+
return code >= 97 && code <= 122 || // a-z
|
|
68
|
+
code >= 65 && code <= 90 || // A-Z
|
|
69
|
+
code >= 48 && code <= 57 || // 0-9
|
|
70
|
+
code === 95;
|
|
71
|
+
}
|
|
72
|
+
function collapseWhitespace(text) {
|
|
73
|
+
let out = "";
|
|
74
|
+
let pendingSpace = false;
|
|
75
|
+
for (const ch of text) {
|
|
76
|
+
if (JS_WHITESPACE.has(ch)) {
|
|
77
|
+
if (out.length > 0) pendingSpace = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (pendingSpace) {
|
|
81
|
+
out += " ";
|
|
82
|
+
pendingSpace = false;
|
|
83
|
+
}
|
|
84
|
+
out += ch;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function isReadablePlaceholder(name) {
|
|
89
|
+
if (name.length < 2) return false;
|
|
90
|
+
if (name.charCodeAt(0) !== 37) return false;
|
|
91
|
+
const last = name.charCodeAt(name.length - 1);
|
|
92
|
+
if (!isWordCharCode(last)) return false;
|
|
93
|
+
for (let i = 1; i < name.length - 1; i++) {
|
|
94
|
+
const code = name.charCodeAt(i);
|
|
95
|
+
if (code === 45 || isWordCharCode(code)) continue;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function containsStatementDelimiter(name) {
|
|
101
|
+
for (let i = 0; i < name.length; i++) {
|
|
102
|
+
const code = name.charCodeAt(i);
|
|
103
|
+
if (code === 59 || code === 123 || code === 125) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function mixinName(name) {
|
|
108
|
+
return `@mixin ${name}`;
|
|
109
|
+
}
|
|
110
|
+
function functionName(name) {
|
|
111
|
+
return `@function ${name}`;
|
|
112
|
+
}
|
|
113
|
+
var RECOVERY_CHILD_CAP = 1e4;
|
|
114
|
+
var SCOPE_NODES = /* @__PURE__ */ new Set([
|
|
115
|
+
"rule_set",
|
|
116
|
+
"mixin_statement",
|
|
117
|
+
"function_statement",
|
|
118
|
+
"if_statement",
|
|
119
|
+
"each_statement",
|
|
120
|
+
"for_statement",
|
|
121
|
+
"while_statement"
|
|
122
|
+
]);
|
|
123
|
+
function hasScopeAncestor(node) {
|
|
124
|
+
for (let p = node.parent; p; p = p.parent) if (SCOPE_NODES.has(p.type)) return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function isPrivateSassName(published) {
|
|
128
|
+
const bare = published.startsWith("$") ? published.slice(1) : published.startsWith("@mixin ") ? published.slice("@mixin ".length) : published.startsWith("@function ") ? published.slice("@function ".length) : published;
|
|
129
|
+
return bare.startsWith("-") || bare.startsWith("_");
|
|
130
|
+
}
|
|
131
|
+
function hasSchemeHead(specifier) {
|
|
132
|
+
const colon = specifier.indexOf(":");
|
|
133
|
+
if (colon <= 0) return false;
|
|
134
|
+
const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
135
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
136
|
+
if (!isAlpha(specifier.charCodeAt(0))) return false;
|
|
137
|
+
for (let i = 1; i < colon; i++) {
|
|
138
|
+
const c = specifier.charCodeAt(i);
|
|
139
|
+
if (!isAlpha(c) && !isDigit(c) && c !== 43 && c !== 46 && c !== 45) return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
function hasNodeModulesSegment(specifier) {
|
|
144
|
+
return specifier.startsWith("node_modules/") || specifier.includes("/node_modules/");
|
|
145
|
+
}
|
|
146
|
+
function isScopedPackageSpecifier(specifier) {
|
|
147
|
+
if (!specifier.startsWith("@")) return false;
|
|
148
|
+
const slash = specifier.indexOf("/", 1);
|
|
149
|
+
if (slash < 2) return false;
|
|
150
|
+
const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
151
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
152
|
+
for (let i = 1; i < slash; i++) {
|
|
153
|
+
const c = specifier.charCodeAt(i);
|
|
154
|
+
if (!isAlpha(c) && !isDigit(c) && c !== 46 && c !== 95 && c !== 45) return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function hasCssFileExtension(specifier) {
|
|
159
|
+
const lower = specifier.toLowerCase();
|
|
160
|
+
let at = lower.indexOf(".css");
|
|
161
|
+
while (at >= 0) {
|
|
162
|
+
const next = lower.charAt(at + 4);
|
|
163
|
+
if (next === "" || next === "?" || next === "#") return true;
|
|
164
|
+
at = lower.indexOf(".css", at + 4);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function includeStatementIsComplete(statementText) {
|
|
169
|
+
const trimmed = statementText.trimEnd();
|
|
170
|
+
if (trimmed.endsWith(";")) return true;
|
|
171
|
+
if (!trimmed.endsWith("}")) return false;
|
|
172
|
+
return trimmed.indexOf("{") >= 0;
|
|
173
|
+
}
|
|
174
|
+
function normalizeModuleSpelling(spelling) {
|
|
175
|
+
const segments = spelling.split("\\").join("/").split("/");
|
|
176
|
+
let leaf = segments.length > 0 ? segments[segments.length - 1] ?? spelling : spelling;
|
|
177
|
+
const lower = leaf.toLowerCase();
|
|
178
|
+
for (const ext of [".scss", ".sass", ".css"]) {
|
|
179
|
+
if (lower.endsWith(ext)) {
|
|
180
|
+
leaf = leaf.slice(0, leaf.length - ext.length);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (leaf.startsWith("_")) leaf = leaf.slice(1);
|
|
185
|
+
return leaf;
|
|
186
|
+
}
|
|
187
|
+
function isExternalImportSpecifier(written) {
|
|
188
|
+
if (hasSchemeHead(written) || written.startsWith("//")) return true;
|
|
189
|
+
if (hasNodeModulesSegment(written)) return true;
|
|
190
|
+
if (written.startsWith("~")) return true;
|
|
191
|
+
if (isScopedPackageSpecifier(written)) return true;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
function sourcePathClaimIsUnrefutable(projectRoot, written) {
|
|
195
|
+
const candidates = [];
|
|
196
|
+
const push = (v) => {
|
|
197
|
+
const t = v.trim();
|
|
198
|
+
if (t.length > 0 && !candidates.includes(t)) candidates.push(t);
|
|
199
|
+
};
|
|
200
|
+
push(written);
|
|
201
|
+
push(written.startsWith("./") ? written.slice(2) : written);
|
|
202
|
+
push(written.startsWith("../") ? written.slice(3) : written);
|
|
203
|
+
const segs = written.split("/").filter((x) => x.length > 0 && x !== "." && x !== "..");
|
|
204
|
+
if (segs.length > 0) {
|
|
205
|
+
push(segs.join("/"));
|
|
206
|
+
push(segs[segs.length - 1]);
|
|
207
|
+
}
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
try {
|
|
210
|
+
if (existsSync(path.resolve(projectRoot, candidate))) return false;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
var RealScssAnalyzer = class {
|
|
218
|
+
analyze(rootNode, content, filePath, context) {
|
|
219
|
+
const base = new RealCssAnalyzer().analyze(rootNode, content, filePath);
|
|
220
|
+
const ctx = {
|
|
221
|
+
symbols: base.symbols ?? [],
|
|
222
|
+
relationships: base.relationships ?? [],
|
|
223
|
+
imports: base.imports ?? [],
|
|
224
|
+
exports: base.exports ?? [],
|
|
225
|
+
mixinNames: {},
|
|
226
|
+
functionNames: {},
|
|
227
|
+
variableNames: {},
|
|
228
|
+
namespaces: {},
|
|
229
|
+
sassPartImports: /* @__PURE__ */ new Set(),
|
|
230
|
+
published: {},
|
|
231
|
+
publishedOwner: {},
|
|
232
|
+
moduleName: this.moduleNameOf(filePath),
|
|
233
|
+
topLevelVarLines: {},
|
|
234
|
+
sawDiagnostic: false,
|
|
235
|
+
sawContent: false,
|
|
236
|
+
sawAtRoot: false,
|
|
237
|
+
projectIndex: context?.projectIndex
|
|
238
|
+
};
|
|
239
|
+
const baseModule = ctx.symbols.find(
|
|
240
|
+
(s) => s.kind === ASTEngine.SymbolKind.Module && s.name === ctx.moduleName
|
|
241
|
+
);
|
|
242
|
+
if (baseModule) {
|
|
243
|
+
baseModule.metadata = {
|
|
244
|
+
...baseModule.metadata ?? {},
|
|
245
|
+
isScssModule: true,
|
|
246
|
+
scssModuleName: ctx.moduleName,
|
|
247
|
+
scssModulePath: filePath
|
|
248
|
+
};
|
|
249
|
+
} else {
|
|
250
|
+
ctx.symbols.unshift({
|
|
251
|
+
name: ctx.moduleName,
|
|
252
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
253
|
+
range: this.nodeToRange(rootNode),
|
|
254
|
+
scope: "",
|
|
255
|
+
visibility: "public",
|
|
256
|
+
complexity: 1,
|
|
257
|
+
metadata: { cssSpecific: true, isScssModule: true, scssModuleName: ctx.moduleName, scssModulePath: filePath }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
for (const s of ctx.symbols) ctx.published[s.name] = true;
|
|
261
|
+
this.collectDeclarations(rootNode, ctx);
|
|
262
|
+
this.walk(rootNode, ctx);
|
|
263
|
+
if (ctx.recoveryCap && ctx.relationships.length > 0) {
|
|
264
|
+
ctx.relationships[0].metadata = { ...ctx.relationships[0].metadata ?? {}, scssRecoveryCap: ctx.recoveryCap };
|
|
265
|
+
}
|
|
266
|
+
this.emitModuleExports(ctx);
|
|
267
|
+
const claimRoot = typeof context?.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
268
|
+
if (claimRoot !== null) {
|
|
269
|
+
const fileRel = path.relative(path.resolve(claimRoot), filePath).split(path.sep).join("/");
|
|
270
|
+
const baseDir = dirOf(fileRel);
|
|
271
|
+
for (const r of ctx.relationships) {
|
|
272
|
+
if (r.type !== ASTEngine.RelationshipType.Imports) continue;
|
|
273
|
+
const md = r.metadata;
|
|
274
|
+
if (!md || typeof md.importProjectFile === "string") continue;
|
|
275
|
+
const written = String(r.target ?? "").trim();
|
|
276
|
+
if (written.length === 0) continue;
|
|
277
|
+
const cut = written.lastIndexOf("/");
|
|
278
|
+
const dirPart = cut < 0 ? "" : written.slice(0, cut + 1);
|
|
279
|
+
const leaf = cut < 0 ? written : written.slice(cut + 1);
|
|
280
|
+
const spellings = /* @__PURE__ */ new Set([`${written}.scss`, `${written}/_index.scss`, `${written}/index.scss`]);
|
|
281
|
+
if (leaf.length > 0 && !leaf.startsWith("_")) {
|
|
282
|
+
spellings.add(`${dirPart}_${leaf}.scss`);
|
|
283
|
+
spellings.add(`${dirPart}_${leaf}/_index.scss`);
|
|
284
|
+
}
|
|
285
|
+
const extra = [];
|
|
286
|
+
for (const sp of spellings) {
|
|
287
|
+
for (const cand of [foldProjectPath(baseDir, sp), foldProjectPath("", sp)]) {
|
|
288
|
+
if (cand !== null && !extra.includes(cand)) extra.push(cand);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const claim = fileClaimForTarget({
|
|
292
|
+
projectRoot: claimRoot,
|
|
293
|
+
filePath,
|
|
294
|
+
written,
|
|
295
|
+
admittedExtensions: ["scss"],
|
|
296
|
+
// THE POPULATION'S SIZE CAP, passed because this lane HAS over-cap files (4 for graphql,
|
|
297
|
+
// 2 for scss on the gate's own population line). A claim about one of them would be
|
|
298
|
+
// REFUTED, and nothing in the row would say why.
|
|
299
|
+
maxFileSizeBytes: FILE_CLAIM_MAX_BYTES,
|
|
300
|
+
extraCandidates: extra
|
|
301
|
+
});
|
|
302
|
+
if (claim !== null) {
|
|
303
|
+
md.importProjectFile = claim;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (isExternalImportSpecifier(written) && sourcePathClaimIsUnrefutable(claimRoot, written)) {
|
|
307
|
+
md.sourcePathClaim = [written];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.relabelSassPartImports(ctx);
|
|
312
|
+
this.publishScssFileScopedIdentities(ctx, filePath);
|
|
313
|
+
return {
|
|
314
|
+
...base,
|
|
315
|
+
symbols: ctx.symbols,
|
|
316
|
+
relationships: ctx.relationships,
|
|
317
|
+
imports: ctx.imports,
|
|
318
|
+
exports: ctx.exports,
|
|
319
|
+
metadata: {
|
|
320
|
+
...base.metadata ?? {},
|
|
321
|
+
scssDialect: true,
|
|
322
|
+
scssMixins: Object.keys(ctx.mixinNames).length,
|
|
323
|
+
scssFunctions: Object.keys(ctx.functionNames).length,
|
|
324
|
+
scssVariables: Object.keys(ctx.variableNames).length,
|
|
325
|
+
scssModuleName: ctx.moduleName,
|
|
326
|
+
scssFrameworks: ["SCSS/SASS"],
|
|
327
|
+
...ctx.sawDiagnostic ? { scssHasDiagnostics: true } : {},
|
|
328
|
+
...ctx.sawContent ? { scssForwardsContent: true } : {},
|
|
329
|
+
...ctx.sawAtRoot ? { scssEscapesScope: true } : {}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** The stylesheet's own module name: its basename, extension included. */
|
|
334
|
+
moduleNameOf(filePath) {
|
|
335
|
+
const normalized = filePath.split("\\").join("/");
|
|
336
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
337
|
+
return base.length > 0 ? base : "module.scss";
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Give the symbols THIS analyzer added — and only those — a project-unique `qualifiedName`,
|
|
341
|
+
* then respell the edges that name one of them from the file that declares it.
|
|
342
|
+
*
|
|
343
|
+
* WHY ONLY THE ONES THIS ANALYZER ADDED. The delegate already ran
|
|
344
|
+
* `publishFileScopedIdentities` over its own output, so every symbol it published already
|
|
345
|
+
* carries a `qualifiedName` and every edge it could respell already has. A target that is
|
|
346
|
+
* STILL bare after the delegate declined it (no local declaration, or several and no line to
|
|
347
|
+
* choose between them) is a decision the delegate made deliberately, and re-deciding it here
|
|
348
|
+
* would be a second implementation of a rule this lane is supposed to inherit. So the pass
|
|
349
|
+
* touches a symbol only when `qualifiedName` is ABSENT, which is exactly the set added here:
|
|
350
|
+
* `$variables`, `@mixin`/`@function` symbols, the module's namespaces, the control-flow
|
|
351
|
+
* `interface` symbols, the imported-member symbols, and the module row itself.
|
|
352
|
+
*
|
|
353
|
+
* WHY REFERENCES/EXPORTS/CALLS/USES/OVERRIDES. `Exports` and `Calls` are the two the delegate
|
|
354
|
+
* has no analogue for (CSS has neither a module surface nor a call), and they are the two
|
|
355
|
+
* carrying this lane's measured collisions; `References` and `Uses` are here because the SCSS
|
|
356
|
+
* reads (`$var`, `@include`) are pushed AFTER the delegate's pass and so were never seen by
|
|
357
|
+
* it. `Imports` is deliberately ABSENT for the delegate's own stated reason: an `imports`
|
|
358
|
+
* edge names ANOTHER file's module, and this file's module symbol happens to share its
|
|
359
|
+
* spelling with that file's basename — respelling it would point the import at the importing
|
|
360
|
+
* file itself.
|
|
361
|
+
*/
|
|
362
|
+
publishScssFileScopedIdentities(ctx, filePath) {
|
|
363
|
+
const scope = cssFileScopeOf(filePath);
|
|
364
|
+
if (scope === "") return;
|
|
365
|
+
const byName = /* @__PURE__ */ new Map();
|
|
366
|
+
for (const sym of ctx.symbols) {
|
|
367
|
+
const name = String(sym.name ?? "");
|
|
368
|
+
if (name.length === 0) continue;
|
|
369
|
+
if (sym.qualifiedName !== void 0 && sym.qualifiedName !== "") continue;
|
|
370
|
+
sym.qualifiedName = `${scope}::${name}`;
|
|
371
|
+
if (!byName.has(name)) byName.set(name, String(sym.qualifiedName));
|
|
372
|
+
}
|
|
373
|
+
if (byName.size === 0) return;
|
|
374
|
+
const REWRITABLE = /* @__PURE__ */ new Set([
|
|
375
|
+
ASTEngine.RelationshipType.References,
|
|
376
|
+
ASTEngine.RelationshipType.Exports,
|
|
377
|
+
ASTEngine.RelationshipType.Calls,
|
|
378
|
+
ASTEngine.RelationshipType.Uses,
|
|
379
|
+
ASTEngine.RelationshipType.Overrides,
|
|
380
|
+
// `mixes_in` targets a `@mixin <name>` symbol THIS FILE may declare, so the same file-scoped
|
|
381
|
+
// respell applies — and it must: a target published bare for a mixin the file declares is the
|
|
382
|
+
// collision shape this pass exists to remove. `part_of` is deliberately ABSENT for the reason
|
|
383
|
+
// `Imports` is: its target names ANOTHER file's module, and respelling it to this file's
|
|
384
|
+
// scope would point the inclusion at the including file itself.
|
|
385
|
+
ASTEngine.RelationshipType.MixesIn
|
|
386
|
+
]);
|
|
387
|
+
for (const rel of ctx.relationships) {
|
|
388
|
+
if (!REWRITABLE.has(String(rel.type ?? ""))) continue;
|
|
389
|
+
const target = String(rel.target ?? "");
|
|
390
|
+
if (target.length === 0 || target.includes("::")) continue;
|
|
391
|
+
const qname = byName.get(target);
|
|
392
|
+
if (qname !== void 0) rel.target = qname;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
396
|
+
lineOf(node) {
|
|
397
|
+
return node ? node.startPosition.row + 1 : void 0;
|
|
398
|
+
}
|
|
399
|
+
isErrorNode(node) {
|
|
400
|
+
return node.type === "ERROR" || node.isMissing === true;
|
|
401
|
+
}
|
|
402
|
+
findChild(node, predicate) {
|
|
403
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
404
|
+
const c = node.namedChild(i);
|
|
405
|
+
if (c && predicate(c)) return c;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
findDescendant(node, predicate) {
|
|
410
|
+
const queue = [node];
|
|
411
|
+
while (queue.length > 0) {
|
|
412
|
+
const n = queue.shift();
|
|
413
|
+
if (predicate(n)) return n;
|
|
414
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
415
|
+
const c = n.namedChild(i);
|
|
416
|
+
if (c) queue.push(c);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The byte offset where an `@extend` statement's own selector stops.
|
|
423
|
+
*
|
|
424
|
+
* That is the first `;` at or after the statement starts, because `;` terminates the
|
|
425
|
+
* declaration the `@extend` belongs to and everything past it is the next declaration. A
|
|
426
|
+
* statement with no `;` is bounded by its own end.
|
|
427
|
+
*
|
|
428
|
+
* Returned as an OFFSET rather than used as a string test on the target, so a legitimate
|
|
429
|
+
* `@extend` whose target happens to contain `;`-adjacent punctuation is judged by position
|
|
430
|
+
* in the source and not by a shape.
|
|
431
|
+
*/
|
|
432
|
+
extendTargetBound(node) {
|
|
433
|
+
const semi = String(node.text ?? "").indexOf(";");
|
|
434
|
+
return semi < 0 ? node.endIndex : node.startIndex + semi;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Every strict descendant matching `predicate`, in tree order.
|
|
438
|
+
*
|
|
439
|
+
* Needed where `findDescendant`'s breadth-first FIRST match is the wrong node: on a
|
|
440
|
+
* broken `@extend` the first match is a `class_selector` spanning the rest of the region
|
|
441
|
+
* and the real referent is a `placeholder` nested inside it (see `extendedSelectorName`).
|
|
442
|
+
* Depth-first, so the innermost usable node is preferred over a shallower one.
|
|
443
|
+
*/
|
|
444
|
+
descendants(node, predicate) {
|
|
445
|
+
const out = [];
|
|
446
|
+
const stack = [node];
|
|
447
|
+
while (stack.length > 0) {
|
|
448
|
+
const n = stack.pop();
|
|
449
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
450
|
+
const c = n.namedChild(i);
|
|
451
|
+
if (!c) continue;
|
|
452
|
+
if (predicate(c)) out.push(c);
|
|
453
|
+
stack.push(c);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
nodeToRange(node) {
|
|
459
|
+
return {
|
|
460
|
+
startLine: node.startPosition.row + 1,
|
|
461
|
+
startColumn: node.startPosition.column,
|
|
462
|
+
endLine: node.endPosition.row + 1,
|
|
463
|
+
endColumn: node.endPosition.column,
|
|
464
|
+
startIndex: node.startIndex,
|
|
465
|
+
endIndex: node.endIndex
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
childForField(node, field) {
|
|
469
|
+
return node.childForFieldName(field);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The innermost PUBLISHED symbol whose range contains `node` — i.e. the rule,
|
|
473
|
+
* mixin or function this statement sits inside.
|
|
474
|
+
*
|
|
475
|
+
* WHY A RANGE LOOKUP AND NOT A TRACKED VARIABLE. The first version threaded an
|
|
476
|
+
* `owner` argument through the walk, the way the CSS analyzer threads
|
|
477
|
+
* `currentSelector`, and it produced three fabrications in one file:
|
|
478
|
+
*
|
|
479
|
+
* uses button -> @mixin button (source was the mixin NAME)
|
|
480
|
+
* uses .btn -> .btn (source was its own target)
|
|
481
|
+
* references color: $brand; -> $brand (source was raw declaration text)
|
|
482
|
+
*
|
|
483
|
+
* All three have a source that is not an entity. The reason is that a tracked
|
|
484
|
+
* variable only advances for the node types the walker recognises, and a `rule_set`
|
|
485
|
+
* carrying an `@include` is not one of them — so inside any ordinary rule the owner
|
|
486
|
+
* was empty and each edge fell back to something adjacent.
|
|
487
|
+
*
|
|
488
|
+
* Looking the owner up by RANGE fixes all three at once and, more importantly, makes
|
|
489
|
+
* the edge name the SAME symbol the CSS lane published for that rule. Recomputing a
|
|
490
|
+
* selector name here would be a second implementation of the CSS lane's naming —
|
|
491
|
+
* including its `&`-chain resolution — and two implementations of a naming rule is
|
|
492
|
+
* how a graph ends up with `.card` on one side of an edge and `div .card` on the
|
|
493
|
+
* other. Innermost wins, so a declaration inside a rule inside a mixin is owned by the
|
|
494
|
+
* rule, and a statement directly in a mixin body is owned by the mixin.
|
|
495
|
+
*/
|
|
496
|
+
ownerForNode(node, ctx) {
|
|
497
|
+
const start = node.startIndex;
|
|
498
|
+
const idx = this.ownerIndexOf(ctx);
|
|
499
|
+
const pick = {
|
|
500
|
+
entry: null,
|
|
501
|
+
span: Number.POSITIVE_INFINITY,
|
|
502
|
+
orig: Number.POSITIVE_INFINITY
|
|
503
|
+
};
|
|
504
|
+
const consider = (e) => {
|
|
505
|
+
if (start < e.start || start >= e.end) return;
|
|
506
|
+
const span = e.end - e.start;
|
|
507
|
+
if (span < pick.span || span === pick.span && e.orig < pick.orig) {
|
|
508
|
+
pick.span = span;
|
|
509
|
+
pick.entry = e;
|
|
510
|
+
pick.orig = e.orig;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
let lo = 0;
|
|
514
|
+
let hi = idx.sorted.length - 1;
|
|
515
|
+
let j = -1;
|
|
516
|
+
while (lo <= hi) {
|
|
517
|
+
const mid = lo + hi >> 1;
|
|
518
|
+
if (idx.sorted[mid].start <= start) {
|
|
519
|
+
j = mid;
|
|
520
|
+
lo = mid + 1;
|
|
521
|
+
} else hi = mid - 1;
|
|
522
|
+
}
|
|
523
|
+
for (let i = j; i >= 0; i--) {
|
|
524
|
+
if (idx.maxEnd[i] <= start) break;
|
|
525
|
+
consider(idx.sorted[i]);
|
|
526
|
+
}
|
|
527
|
+
for (const e of idx.tail) consider(e);
|
|
528
|
+
return pick.entry ? pick.entry.sym.name : "";
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Build (or grow) the interval index `ownerForNode` reads. Rebuilds happen when the un-indexed
|
|
532
|
+
* tail grows past 32 entries, and whenever the identity guard sees the array reordered — so the
|
|
533
|
+
* index can only ever be a faster spelling of the scan it replaced.
|
|
534
|
+
*/
|
|
535
|
+
ownerIndexOf(ctx) {
|
|
536
|
+
const symbols = ctx.symbols;
|
|
537
|
+
let idx = ctx.ownerIndex;
|
|
538
|
+
if (!idx || idx.len > 0 && (idx.len > symbols.length || symbols[idx.len - 1] !== idx.lastSeen)) {
|
|
539
|
+
idx = ctx.ownerIndex = { len: 0, lastSeen: void 0, sorted: [], maxEnd: [], tail: [] };
|
|
540
|
+
}
|
|
541
|
+
if (symbols.length === idx.len) return idx;
|
|
542
|
+
for (let i = idx.len; i < symbols.length; i++) {
|
|
543
|
+
const sym = symbols[i];
|
|
544
|
+
if (sym.metadata?.isScssModule) continue;
|
|
545
|
+
const r = sym.range;
|
|
546
|
+
if (!r || typeof r.startIndex !== "number" || typeof r.endIndex !== "number") continue;
|
|
547
|
+
idx.tail.push({ sym, orig: i, start: r.startIndex, end: r.endIndex });
|
|
548
|
+
}
|
|
549
|
+
idx.len = symbols.length;
|
|
550
|
+
idx.lastSeen = symbols[symbols.length - 1];
|
|
551
|
+
if (idx.tail.length > 32) {
|
|
552
|
+
idx.sorted = idx.sorted.concat(idx.tail).sort((a, b) => a.start - b.start || a.orig - b.orig);
|
|
553
|
+
idx.tail = [];
|
|
554
|
+
idx.maxEnd = new Array(idx.sorted.length);
|
|
555
|
+
let m = Number.NEGATIVE_INFINITY;
|
|
556
|
+
for (let i = 0; i < idx.sorted.length; i++) {
|
|
557
|
+
if (idx.sorted[i].end > m) m = idx.sorted[i].end;
|
|
558
|
+
idx.maxEnd[i] = m;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return idx;
|
|
562
|
+
}
|
|
563
|
+
/** The old linear scan, kept so the equivalence harness can compare against it (never used in the
|
|
564
|
+
* analysis path). Any difference between this and `ownerForNode` is a regression. */
|
|
565
|
+
ownerForNodeLinear(node, ctx) {
|
|
566
|
+
const start = node.startIndex;
|
|
567
|
+
let best = null;
|
|
568
|
+
let bestSpan = Number.POSITIVE_INFINITY;
|
|
569
|
+
for (const sym of ctx.symbols) {
|
|
570
|
+
if (sym.metadata?.isScssModule) continue;
|
|
571
|
+
const r = sym.range;
|
|
572
|
+
if (!r || typeof r.startIndex !== "number" || typeof r.endIndex !== "number") continue;
|
|
573
|
+
if (start < r.startIndex || start >= r.endIndex) continue;
|
|
574
|
+
const span = r.endIndex - r.startIndex;
|
|
575
|
+
if (span < bestSpan) {
|
|
576
|
+
bestSpan = span;
|
|
577
|
+
best = sym;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return best ? best.name : "";
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* A declaration is published ONCE per name. `$brand: #f00` in three places is one
|
|
584
|
+
* variable that is assigned three times, and the CSS lane's identity ruling —
|
|
585
|
+
* `(kind, resolved name)`, scope retained as provenance — applies unchanged: a
|
|
586
|
+
* symbol that exists twice with one name is the identity collision this project has
|
|
587
|
+
* been removing all along, and `.scss` is full of re-assignments.
|
|
588
|
+
*/
|
|
589
|
+
publishOnce(ctx, symbol, owner = "") {
|
|
590
|
+
const key = `${symbol.kind} ${symbol.name}`;
|
|
591
|
+
if (!ctx.published[key]) {
|
|
592
|
+
ctx.published[key] = true;
|
|
593
|
+
ctx.publishedOwner[key] = owner;
|
|
594
|
+
ctx.symbols.push(symbol);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
if (ctx.publishedOwner[key] === owner) return false;
|
|
598
|
+
let n = 2;
|
|
599
|
+
while (ctx.published[`${key}#${n}`]) n++;
|
|
600
|
+
const disambiguated = { ...symbol, name: `${symbol.name}#${n}` };
|
|
601
|
+
ctx.published[`${key}#${n}`] = true;
|
|
602
|
+
ctx.publishedOwner[`${key}#${n}`] = owner;
|
|
603
|
+
ctx.symbols.push(disambiguated);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
// ── the module's public surface: `exports` ─────────────────────────────────
|
|
607
|
+
/**
|
|
608
|
+
* A Sass module exports its top-level variables, mixins and functions, and nothing
|
|
609
|
+
* else — that is precisely what `@use 'this-file'` binds. An assignment inside a rule,
|
|
610
|
+
* a mixin, a function or a control-flow block is LOCAL and is not part of the surface.
|
|
611
|
+
*
|
|
612
|
+
* Measured over 4,394 admitted corpus files: 3,315 top-level `$` assignments naming
|
|
613
|
+
* 2,501 distinct variables, 30 of them private by the `-`/`_` rule, plus 308 mixin and
|
|
614
|
+
* 77 function declarations — so this emits roughly one edge per distinct public member
|
|
615
|
+
* of every stylesheet, which is the module-system pair with `imports`.
|
|
616
|
+
*
|
|
617
|
+
* BOTH ends resolve by construction: the source is this file's own module symbol and
|
|
618
|
+
* the target is a symbol this file publishes. That is the whole reason to model a
|
|
619
|
+
* module's surface as EDGES rather than as a convention: `@use 'a', 'b'` binds
|
|
620
|
+
* members from two files and nothing else in the graph can say which file offered a
|
|
621
|
+
* name.
|
|
622
|
+
*/
|
|
623
|
+
emitModuleExports(ctx) {
|
|
624
|
+
const published = /* @__PURE__ */ new Set();
|
|
625
|
+
for (const sym of ctx.symbols) {
|
|
626
|
+
if (sym.metadata?.isScssModule) continue;
|
|
627
|
+
if (sym.metadata?.scssImportedFrom) continue;
|
|
628
|
+
if (sym.kind !== ASTEngine.SymbolKind.Variable && sym.kind !== ASTEngine.SymbolKind.Function) continue;
|
|
629
|
+
const isCallable = sym.kind === ASTEngine.SymbolKind.Function;
|
|
630
|
+
if (isCallable ? !sym.metadata?.scssCallable : !sym.metadata?.isScssVariable) continue;
|
|
631
|
+
if (sym.scope) continue;
|
|
632
|
+
const name = sym.name;
|
|
633
|
+
if (published.has(name)) continue;
|
|
634
|
+
if (isPrivateSassName(name)) {
|
|
635
|
+
published.add(name);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
published.add(name);
|
|
639
|
+
ctx.relationships.push({
|
|
640
|
+
source: ctx.moduleName,
|
|
641
|
+
target: name,
|
|
642
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
643
|
+
line_number: sym.range?.startLine ?? 1,
|
|
644
|
+
confidence: 1,
|
|
645
|
+
metadata: {
|
|
646
|
+
evidenceClass: "structural",
|
|
647
|
+
cssSpecific: true,
|
|
648
|
+
isScssModuleMember: true,
|
|
649
|
+
scssModule: ctx.moduleName
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
ctx.exports.push({
|
|
653
|
+
name,
|
|
654
|
+
kind: sym.kind,
|
|
655
|
+
range: sym.range,
|
|
656
|
+
visibility: "public",
|
|
657
|
+
metadata: { via: "sass-module-top-level", scssModule: ctx.moduleName }
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// ── `overrides`: the one reassignment Sass itself calls an override ─────────
|
|
662
|
+
/**
|
|
663
|
+
* `$x: … !global` where `$x` was already assigned at this file's top level.
|
|
664
|
+
*
|
|
665
|
+
* This is the narrowest honest `overrides` in the language and every part of it is
|
|
666
|
+
* measured, because the wide readings are wrong:
|
|
667
|
+
*
|
|
668
|
+
* * A SECOND `@mixin x` / `@function x`: 0 occurrences across 4,394 admitted files.
|
|
669
|
+
* The branch exists and cannot fire on this corpus, which is a fact about the
|
|
670
|
+
* corpus and not a claim that the construct does not exist.
|
|
671
|
+
* * A plain same-scope reassignment: 203 occurrences, of which 171 READ the variable
|
|
672
|
+
* they assign (`$_args: append($_args, …)` in bootstrap's `_functions.scss` is the
|
|
673
|
+
* shape) — an accumulator mutating one binding, which is not the replacement of a
|
|
674
|
+
* declaration and would be a fabricated edge. The other 32 are a `$x: 1; … $x: 2`
|
|
675
|
+
* pair, which the lane's own identity ruling already collapses into ONE symbol, so
|
|
676
|
+
* an edge between the two assignments would be a self-edge.
|
|
677
|
+
* * `!global`: 7 occurrences, and it is the only one of the three that means what
|
|
678
|
+
* `overrides` means. `!global` is the flag that reaches PAST the enclosing scope
|
|
679
|
+
* and reassigns the binding outside it, so the prior declaration is replaced and
|
|
680
|
+
* the two are different things. bootstrap's `_box-shadow.test.scss` is the model:
|
|
681
|
+
* `$enable-shadows: false !global;` … `$enable-shadows: true !global;` … and a
|
|
682
|
+
* final restore, three overrides of one module-level variable.
|
|
683
|
+
*
|
|
684
|
+
* The prior assignment must be a TOP-LEVEL one. `!global` beside a name that only
|
|
685
|
+
* exists inside another mixin CREATES a global rather than replacing one, and the 14
|
|
686
|
+
* `!global` assignments in element-plus that fail this test are exactly that.
|
|
687
|
+
*
|
|
688
|
+
* The edge runs from the scope that performs the assignment to the variable it
|
|
689
|
+
* replaces, with `overridesLine` carrying the replaced declaration's line, so a
|
|
690
|
+
* consumer does not have to infer it. `source` falls back to the module symbol for an
|
|
691
|
+
* assignment at the file's top level, which is the only case with no enclosing scope.
|
|
692
|
+
*/
|
|
693
|
+
emitGlobalOverride(node, name, ctx) {
|
|
694
|
+
if (!this.hasVisibilityFlag(node, "global")) return;
|
|
695
|
+
const line = this.lineOf(node) ?? 0;
|
|
696
|
+
const prior = ctx.topLevelVarLines[name];
|
|
697
|
+
if (prior === void 0 || prior >= line) return;
|
|
698
|
+
const owner = this.ownerForNode(node, ctx) || ctx.moduleName;
|
|
699
|
+
if (!owner) return;
|
|
700
|
+
ctx.relationships.push({
|
|
701
|
+
source: owner,
|
|
702
|
+
target: name,
|
|
703
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
704
|
+
line_number: line,
|
|
705
|
+
confidence: 1,
|
|
706
|
+
metadata: {
|
|
707
|
+
evidenceClass: "structural",
|
|
708
|
+
cssSpecific: true,
|
|
709
|
+
isScssGlobalOverride: true,
|
|
710
|
+
overridesLine: prior,
|
|
711
|
+
scssFlagGlobal: true
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
// ── pass 1: declarations, so a use may precede its declaration ─────────────
|
|
716
|
+
collectDeclarations(root, ctx) {
|
|
717
|
+
const stack = [root];
|
|
718
|
+
while (stack.length > 0) {
|
|
719
|
+
const n = stack.pop();
|
|
720
|
+
if (this.isErrorNode(n)) continue;
|
|
721
|
+
if (n.type === "mixin_statement") {
|
|
722
|
+
const name = this.childForField(n, "name") ?? this.findChild(n, (c) => c.type === "identifier");
|
|
723
|
+
if (name) ctx.mixinNames[stripAllQuotes(name.text)] = mixinName(stripAllQuotes(name.text));
|
|
724
|
+
} else if (n.type === "function_statement") {
|
|
725
|
+
const name = this.childForField(n, "name") ?? this.findChild(n, (c) => c.type === "identifier");
|
|
726
|
+
if (name) ctx.functionNames[stripAllQuotes(name.text)] = functionName(stripAllQuotes(name.text));
|
|
727
|
+
} else if (n.type === "property_name" && n.text.startsWith("$") && n.parent?.type === "declaration") {
|
|
728
|
+
ctx.variableNames[n.text] = n.text;
|
|
729
|
+
} else if (n.type === "use_statement") {
|
|
730
|
+
const module = this.findChild(n, (c) => c.type === "string_value");
|
|
731
|
+
const ns = this.findChild(n, (c) => c.type === "plain_value" || c.type === "identifier");
|
|
732
|
+
if (module && ns) ctx.namespaces[stripAllQuotes(ns.text)] = stripAllQuotes(module.text);
|
|
733
|
+
}
|
|
734
|
+
if (n.type === "declaration" && !hasScopeAncestor(n)) {
|
|
735
|
+
const prop = this.findChild(n, (c) => c.type === "property_name");
|
|
736
|
+
const name = prop ? prop.text : "";
|
|
737
|
+
if (name.startsWith("$")) {
|
|
738
|
+
const line = n.startPosition.row + 1;
|
|
739
|
+
const seen = ctx.topLevelVarLines[name];
|
|
740
|
+
if (seen === void 0 || line < seen) ctx.topLevelVarLines[name] = line;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
744
|
+
const c = n.namedChild(i);
|
|
745
|
+
if (c) stack.push(c);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// ── pass 2: the SCSS statements ────────────────────────────────────────────
|
|
750
|
+
/**
|
|
751
|
+
* @param recovered TRUE when this node sits inside a region the grammar could not read.
|
|
752
|
+
* Threaded rather than recomputed because the read/call scan keeps its OWN notion of
|
|
753
|
+
* recovery — it descends into errors on its own — and the two must agree. A claim the
|
|
754
|
+
* outer walk reached by descending into a broken region and then handed to the inner
|
|
755
|
+
* scan is recovered even though the inner scan arrived by a clean path, so the two are
|
|
756
|
+
* OR'd, and that is what makes `scssRecoveredFromError` trustworthy rather than
|
|
757
|
+
* merely present.
|
|
758
|
+
*/
|
|
759
|
+
walk(node, ctx, recovered = false, limit = Number.POSITIVE_INFINITY) {
|
|
760
|
+
if (limit === Number.POSITIVE_INFINITY && node.namedChildCount > RECOVERY_CHILD_CAP) {
|
|
761
|
+
const tally = ctx.recoveryCap ??= { cap: RECOVERY_CHILD_CAP, errorNodes: 0, errorChildren: 0, visited: 0, skipped: 0 };
|
|
762
|
+
tally.errorNodes++;
|
|
763
|
+
tally.errorChildren += node.namedChildCount;
|
|
764
|
+
tally.visited += RECOVERY_CHILD_CAP;
|
|
765
|
+
tally.skipped += node.namedChildCount - RECOVERY_CHILD_CAP;
|
|
766
|
+
limit = RECOVERY_CHILD_CAP;
|
|
767
|
+
}
|
|
768
|
+
for (let i = 0; i < node.namedChildCount && i < limit; i++) {
|
|
769
|
+
const child = node.namedChild(i);
|
|
770
|
+
if (!child) continue;
|
|
771
|
+
if (this.isErrorNode(child)) {
|
|
772
|
+
this.processRecoveredExtend(child, ctx);
|
|
773
|
+
this.walk(child, ctx, true);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (child.type === "at_rule") {
|
|
777
|
+
const kw = this.findChild(child, (c) => c.type === "at_keyword");
|
|
778
|
+
const keyword = kw ? kw.text.trim().toLowerCase() : "";
|
|
779
|
+
if (keyword === "@content") ctx.sawContent = true;
|
|
780
|
+
if (keyword === "@at-root") ctx.sawAtRoot = true;
|
|
781
|
+
}
|
|
782
|
+
switch (child.type) {
|
|
783
|
+
case "use_statement":
|
|
784
|
+
case "forward_statement":
|
|
785
|
+
this.processModuleStatement(child, ctx);
|
|
786
|
+
break;
|
|
787
|
+
case "mixin_statement":
|
|
788
|
+
case "function_statement":
|
|
789
|
+
this.processCallable(child, ctx);
|
|
790
|
+
break;
|
|
791
|
+
case "extend_statement":
|
|
792
|
+
this.processExtend(child, ctx);
|
|
793
|
+
break;
|
|
794
|
+
// The CSS delegate already published this statement's `imports` row. This case only
|
|
795
|
+
// RECORDS the specifiers whose shape is the Sass FILE-INCLUSION form, for the relabel
|
|
796
|
+
// that runs after the delegate's file-claim pass (see `relabelSassPartImports`).
|
|
797
|
+
case "import_statement":
|
|
798
|
+
this.processSassPartImport(child, ctx);
|
|
799
|
+
break;
|
|
800
|
+
case "at_root_statement":
|
|
801
|
+
case "debug_statement":
|
|
802
|
+
case "warn_statement":
|
|
803
|
+
case "error_statement":
|
|
804
|
+
if (child.type === "at_root_statement") ctx.sawAtRoot = true;
|
|
805
|
+
else ctx.sawDiagnostic = true;
|
|
806
|
+
break;
|
|
807
|
+
case "declaration":
|
|
808
|
+
this.processScssDeclaration(child, ctx);
|
|
809
|
+
this.emitVariableReadEdges(child, ctx, recovered);
|
|
810
|
+
break;
|
|
811
|
+
// A `$var` is also read in an `@include`'s ARGUMENT list and in a control
|
|
812
|
+
// flow's condition or iterable, and those are reads like any other:
|
|
813
|
+
// `@include button($brand, 8px)` reads `$brand`. The first version only
|
|
814
|
+
// looked inside `declaration`, so an argument read produced no edge and the
|
|
815
|
+
// variable looked write-only in the graph.
|
|
816
|
+
case "if_statement":
|
|
817
|
+
case "each_statement":
|
|
818
|
+
case "for_statement":
|
|
819
|
+
case "while_statement":
|
|
820
|
+
this.processControlFlow(child, ctx, this.ownerForNode(child, ctx));
|
|
821
|
+
this.emitVariableReadEdges(child, ctx, recovered);
|
|
822
|
+
break;
|
|
823
|
+
case "include_statement":
|
|
824
|
+
this.processInclude(child, ctx);
|
|
825
|
+
this.emitVariableReadEdges(child, ctx, recovered);
|
|
826
|
+
break;
|
|
827
|
+
// `@return` and an `@use … with (…)` configuration are the two remaining
|
|
828
|
+
// places a `$var` can be READ outside a declaration's value, and both were
|
|
829
|
+
// silent. The `return_statement` case was removed when the self-edge was
|
|
830
|
+
// fixed — the edge went, the read scan went with it because the two shared a
|
|
831
|
+
// case — and nothing replaced it.
|
|
832
|
+
//
|
|
833
|
+
// Measured: `element-plus/packages/theme-chalk/src/color/index.scss` reads
|
|
834
|
+
// `$red`, `$green` and `$blue` in a `@return rgb2hex(rgb($red, $green, $blue))`
|
|
835
|
+
// at L19, and the differential reported them MISSING. A function's own body
|
|
836
|
+
// variables were unreadable in the graph: the function published them, and
|
|
837
|
+
// nothing recorded that it consumed them.
|
|
838
|
+
case "return_statement":
|
|
839
|
+
case "use_statement":
|
|
840
|
+
case "forward_statement":
|
|
841
|
+
this.emitVariableReadEdges(child, ctx, recovered);
|
|
842
|
+
break;
|
|
843
|
+
default:
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
this.walk(child, ctx, recovered);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
ownerNameOf(node) {
|
|
850
|
+
const name = this.childForField(node, "name") ?? this.findChild(node, (c) => c.type === "identifier");
|
|
851
|
+
if (!name) return "";
|
|
852
|
+
const raw = stripAllQuotes(name.text);
|
|
853
|
+
return node.type === "function_statement" ? functionName(raw) : mixinName(raw);
|
|
854
|
+
}
|
|
855
|
+
// ── `@use` / `@forward` ─────────────────────────────────────────────────────
|
|
856
|
+
/**
|
|
857
|
+
* An `imports` edge, which is the ontology's type for exactly this — the same one
|
|
858
|
+
* the CSS lane uses for `@import` and for Sass `@use`/`@forward`.
|
|
859
|
+
*
|
|
860
|
+
* The `as` namespace is recorded on the ImportInfo because it is the part a
|
|
861
|
+
* consumer cannot recover from the module path: `@use 'sass:map' as map` and
|
|
862
|
+
* `@use 'sass:map' as m` are the same import and different graphs.
|
|
863
|
+
*/
|
|
864
|
+
processModuleStatement(node, ctx) {
|
|
865
|
+
const moduleNode = this.findChild(node, (c) => c.type === "string_value");
|
|
866
|
+
if (!moduleNode) return;
|
|
867
|
+
const module = stripAllQuotes(moduleNode.text);
|
|
868
|
+
const nsNode = this.findChild(node, (c) => c.type === "plain_value" || c.type === "identifier");
|
|
869
|
+
const namespace = nsNode ? stripAllQuotes(nsNode.text) : void 0;
|
|
870
|
+
if (namespace) {
|
|
871
|
+
ctx.symbols.push({
|
|
872
|
+
name: namespace,
|
|
873
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
874
|
+
range: this.nodeToRange(node),
|
|
875
|
+
scope: "",
|
|
876
|
+
visibility: "public",
|
|
877
|
+
complexity: 1,
|
|
878
|
+
metadata: { isAtRule: true, isScssModuleNamespace: true, scssModule: module, atKeyword: `@${node.type.replace("_statement", "")}` }
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
ctx.imports.push({
|
|
882
|
+
module,
|
|
883
|
+
isNamespaceImport: true,
|
|
884
|
+
alias: namespace,
|
|
885
|
+
path: module,
|
|
886
|
+
range: this.nodeToRange(node),
|
|
887
|
+
metadata: {
|
|
888
|
+
scssModuleType: node.type,
|
|
889
|
+
...namespace ? { scssNamespace: namespace } : {}
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
if (ctx.projectIndex && namespace) {
|
|
893
|
+
this.resolveModuleImports(ctx, namespace, module, this.nodeToRange(node).startLine);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Resolve an imported module's public members from the project index and create
|
|
898
|
+
* exports edges from this file's module to those members.
|
|
899
|
+
* This allows `@use 'other' as ns` to make `ns.$var`, `ns.mixin()`, `ns.function()` resolvable.
|
|
900
|
+
*/
|
|
901
|
+
resolveModuleImports(ctx, namespace, modulePath, importLine) {
|
|
902
|
+
if (!ctx.projectIndex) return;
|
|
903
|
+
const index = ctx.projectIndex;
|
|
904
|
+
if (modulePath.startsWith("sass:")) return;
|
|
905
|
+
const norm = (spelling) => normalizeModuleSpelling(spelling);
|
|
906
|
+
const want = norm(modulePath);
|
|
907
|
+
let targetTypeName;
|
|
908
|
+
if (want !== "") {
|
|
909
|
+
for (const typeName of index.typeMembers.keys()) {
|
|
910
|
+
if (norm(typeName) === want) {
|
|
911
|
+
targetTypeName = typeName;
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!targetTypeName) return;
|
|
917
|
+
const declared = index.typeMembers.get(targetTypeName);
|
|
918
|
+
const closedMembers = index.__closedMembers?.get(targetTypeName) ?? declared;
|
|
919
|
+
if (!closedMembers || closedMembers.size === 0) return;
|
|
920
|
+
let scopePrefix = "";
|
|
921
|
+
for (const sym of ctx.symbols) {
|
|
922
|
+
const q = String(sym.qualifiedName ?? "");
|
|
923
|
+
const sep2 = q.indexOf("::");
|
|
924
|
+
if (sep2 > 0) {
|
|
925
|
+
scopePrefix = q.slice(0, sep2);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (scopePrefix === "") {
|
|
930
|
+
const moduleSym = ctx.symbols.find((sym) => sym.metadata?.isScssModule === true);
|
|
931
|
+
const modulePath2 = String(moduleSym?.metadata?.scssModulePath ?? "");
|
|
932
|
+
if (modulePath2 !== "") scopePrefix = cssFileScopeOf(modulePath2);
|
|
933
|
+
}
|
|
934
|
+
for (const memberName of closedMembers) {
|
|
935
|
+
const qualifiedName = `${namespace}.${memberName}`;
|
|
936
|
+
const scopedName = scopePrefix === "" ? qualifiedName : `${scopePrefix}::${qualifiedName}`;
|
|
937
|
+
ctx.relationships.push({
|
|
938
|
+
source: ctx.moduleName,
|
|
939
|
+
target: scopedName,
|
|
940
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
941
|
+
// THE EDGE'S OWN LINE, not a fabricated 0: the import statement's line is what the edge
|
|
942
|
+
// describes, and `0` is the sentinel this programme has refused everywhere else.
|
|
943
|
+
line_number: importLine ?? 0,
|
|
944
|
+
confidence: 1,
|
|
945
|
+
metadata: {
|
|
946
|
+
evidenceClass: "structural",
|
|
947
|
+
cssSpecific: true,
|
|
948
|
+
isScssModuleMember: true,
|
|
949
|
+
scssModule: ctx.moduleName,
|
|
950
|
+
scssNamespace: namespace,
|
|
951
|
+
scssImportedFrom: modulePath
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
ctx.symbols.push({
|
|
955
|
+
name: qualifiedName,
|
|
956
|
+
// the discriminator rides the PREFIX, never the name — the delegate's own rule
|
|
957
|
+
qualifiedName: scopedName,
|
|
958
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
959
|
+
// conservative; actual kind varies
|
|
960
|
+
range: { startLine: 1, startColumn: 0, endLine: 1, endColumn: 0, startIndex: 0, endIndex: 0 },
|
|
961
|
+
scope: "",
|
|
962
|
+
visibility: "public",
|
|
963
|
+
complexity: 1,
|
|
964
|
+
metadata: {
|
|
965
|
+
isScssVariable: true,
|
|
966
|
+
scssVariableName: qualifiedName,
|
|
967
|
+
scssNamespace: namespace,
|
|
968
|
+
scssImportedFrom: modulePath
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// ── `@mixin` / `@function` ──────────────────────────────────────────────────
|
|
974
|
+
processCallable(node, ctx) {
|
|
975
|
+
const raw = node.type === "function_statement" ? this.childForField(node, "name") : this.childForField(node, "name");
|
|
976
|
+
const nameNode = raw ?? this.findChild(node, (c) => c.type === "identifier");
|
|
977
|
+
if (!nameNode) return;
|
|
978
|
+
const bare = stripAllQuotes(nameNode.text);
|
|
979
|
+
const published = node.type === "function_statement" ? functionName(bare) : mixinName(bare);
|
|
980
|
+
const parameters = this.readParameters(node);
|
|
981
|
+
this.publishOnce(ctx, {
|
|
982
|
+
name: published,
|
|
983
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
984
|
+
range: this.nodeToRange(node),
|
|
985
|
+
scope: "",
|
|
986
|
+
visibility: "public",
|
|
987
|
+
// one step for the declaration, one per parameter, one for the body
|
|
988
|
+
complexity: 1 + parameters.length + (this.findChild(node, (c) => c.type === "block") ? 1 : 0),
|
|
989
|
+
...parameters.length > 0 ? { parameters } : {},
|
|
990
|
+
metadata: {
|
|
991
|
+
isAtRule: true,
|
|
992
|
+
atKeyword: node.type === "function_statement" ? "@function" : "@mixin",
|
|
993
|
+
scssCallable: true,
|
|
994
|
+
...node.type === "function_statement" ? { isScssFunction: true } : { isScssMixin: true },
|
|
995
|
+
parameterCount: parameters.length,
|
|
996
|
+
// whether the body returns a value — see the `@return` note above for why
|
|
997
|
+
// this is a flag and not an edge
|
|
998
|
+
...this.findDescendant(node, (n) => n.type === "return_statement") ? { scssReturns: true } : {}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* A mixin's or function's parameters, read from the `parameter` nodes.
|
|
1004
|
+
*
|
|
1005
|
+
* `defaultValue` is taken from the `default` FIELD rather than by slicing text, so
|
|
1006
|
+
* a parameter whose default is itself an expression keeps the expression rather than
|
|
1007
|
+
* a truncation of it.
|
|
1008
|
+
*/
|
|
1009
|
+
// typed NON-optional on purpose: `UniversalSymbol["parameters"]` is
|
|
1010
|
+
// `Parameter[] | undefined`, so returning it made every `.length` below a
|
|
1011
|
+
// possible-undefined error and the fix would have been three `!` assertions. The
|
|
1012
|
+
// empty case is a real empty list, not an absent one.
|
|
1013
|
+
readParameters(node) {
|
|
1014
|
+
const paramsNode = this.findChild(node, (c) => c.type === "parameters");
|
|
1015
|
+
if (!paramsNode) return [];
|
|
1016
|
+
const out = [];
|
|
1017
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
1018
|
+
const p = paramsNode.namedChild(i);
|
|
1019
|
+
if (!p || p.type !== "parameter") continue;
|
|
1020
|
+
const varNode = this.findChild(p, (c) => c.type === "variable");
|
|
1021
|
+
if (!varNode) continue;
|
|
1022
|
+
const dflt = this.childForField(p, "default");
|
|
1023
|
+
out.push({
|
|
1024
|
+
name: stripAllQuotes(varNode.text),
|
|
1025
|
+
...dflt ? { defaultValue: stripAllQuotes(dflt.text) } : {}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
// ── `@include` ──────────────────────────────────────────────────────────────
|
|
1031
|
+
/**
|
|
1032
|
+
* `@include` — A MIXIN COMPOSITION, PUBLISHED AS `mixes_in`.
|
|
1033
|
+
*
|
|
1034
|
+
* THE CONSTRUCT IS THE VOCABULARY'S MIXIN APPLICATION: a rule (or the module, for a top-level
|
|
1035
|
+
* include) composes a named mixin's body into itself. It is NOT `extends` (there is no
|
|
1036
|
+
* inheritance clause), NOT `implements` (no conformance is declared) and NOT `calls` (the
|
|
1037
|
+
* ontology's own measured population for this type is a composition row — ruby relabelled its
|
|
1038
|
+
* include/extend/prepend from `calls` to `mixes_in` for exactly this reason, and php's trait-use
|
|
1039
|
+
* rows are the second precedent). `@extend` is this lane's `extends`; conflict rules keep the
|
|
1040
|
+
* two apart.
|
|
1041
|
+
*
|
|
1042
|
+
* THE TARGET IS THE WRITTEN MIXIN NAME IN THE LANE'S PUBLISHED SPELLING, `@mixin <name>`, for
|
|
1043
|
+
* EVERY clean include — not only the ones this file declares. The old version published a `calls`
|
|
1044
|
+
* row only when the callee resolved IN THE FILE, which measured 48 of 132 clean includes on a
|
|
1045
|
+
* 247-file screen: an include of a mixin that `@use`/`@import` brought in produced NOTHING, and
|
|
1046
|
+
* the relation was invisible for 64% of the construct's sites. A target that names another
|
|
1047
|
+
* file's mixin is UNRESOLVED rather than absent — the arbiter resolves it through the project
|
|
1048
|
+
* index the way it resolves an `imports` specifier, and the same-file case is respelled to
|
|
1049
|
+
* `<scope>::@mixin <name>` by `publishScssFileScopedIdentities`.
|
|
1050
|
+
*
|
|
1051
|
+
* THE NAMESPACED FORM IS A MEASURED WALL, NOT AN EDGE WE CAN NAME. The served build splits
|
|
1052
|
+
* `@include ns.mixin` at the dot — the `include_statement` closes with a MISSING `;` and the
|
|
1053
|
+
* dotted name lands in an ERROR/selector sibling — so the MIXIN name is not a node this walk can
|
|
1054
|
+
* read, and publishing `@mixin <namespace>` would be a FABRICATED target. This case returns and
|
|
1055
|
+
* the site is COUNTED by `script/m14/scss-extended-census.ts` (`includeUnparsable`), with R2 (a
|
|
1056
|
+
* grammar patch to accept a dotted callable path) named as the available remedy and NOT taken
|
|
1057
|
+
* this round.
|
|
1058
|
+
*/
|
|
1059
|
+
processInclude(node, ctx) {
|
|
1060
|
+
const includeText = String(node.text ?? "");
|
|
1061
|
+
const complete = includeStatementIsComplete(includeText);
|
|
1062
|
+
if (!complete) return;
|
|
1063
|
+
const nameNode = this.findChild(node, (c) => c.type === "identifier" || c.type === "plain_value");
|
|
1064
|
+
if (!nameNode) return;
|
|
1065
|
+
const bare = stripAllQuotes(nameNode.text).trim();
|
|
1066
|
+
if (bare.length === 0 || bare.includes(".")) return;
|
|
1067
|
+
const owner = this.ownerForNode(node, ctx) || ctx.moduleName;
|
|
1068
|
+
if (!owner) return;
|
|
1069
|
+
if (!ctx.mixinNames[bare] && ctx.functionNames[bare]) {
|
|
1070
|
+
ctx.relationships.push({
|
|
1071
|
+
source: owner,
|
|
1072
|
+
target: ctx.functionNames[bare],
|
|
1073
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1074
|
+
line_number: this.lineOf(nameNode),
|
|
1075
|
+
metadata: { evidenceClass: "usage_contextual", cssSpecific: true, isScssInclude: true }
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const sameFileMixin = ctx.mixinNames[bare] !== void 0;
|
|
1080
|
+
ctx.relationships.push({
|
|
1081
|
+
source: owner,
|
|
1082
|
+
target: mixinName(bare),
|
|
1083
|
+
type: ASTEngine.RelationshipType.MixesIn,
|
|
1084
|
+
line_number: this.lineOf(nameNode),
|
|
1085
|
+
// `structural`: the relation is a syntactic declaration-header construct (the keyword plus
|
|
1086
|
+
// the written name), not a name-resolution guess. The tier follows the CONSTRUCT, the same
|
|
1087
|
+
// rule php recorded when its trait-use rows moved off `usage_contextual`.
|
|
1088
|
+
metadata: {
|
|
1089
|
+
evidenceClass: "structural",
|
|
1090
|
+
cssSpecific: true,
|
|
1091
|
+
isScssInclude: true,
|
|
1092
|
+
mixinComposition: true,
|
|
1093
|
+
...sameFileMixin ? {} : { mixinDeclaredInFile: false }
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* RECORD the specifiers whose `@import` shape is the Sass FILE-INCLUSION form.
|
|
1099
|
+
*
|
|
1100
|
+
* THE SHAPE IS THE WHOLE DISCRIMINATOR, and it is a tree fact rather than a string guess:
|
|
1101
|
+
* `@import "x";` → import_statement with EXACTLY ONE named child, a `string_value`
|
|
1102
|
+
* `@import url("x.css")` → the child is a `call_expression` (stylesheet load)
|
|
1103
|
+
* `@import "x" screen` → a second named child (the media query) (stylesheet load)
|
|
1104
|
+
* `@import "x.css";` → one string_value whose TEXT is a CSS file (stylesheet load)
|
|
1105
|
+
* so the Sass form is: one string_value child AND a specifier that is not a `.css` file, not a
|
|
1106
|
+
* protocol/`//` URL, not `sass:` (built-in module) and not `data:`. The relabel itself runs after
|
|
1107
|
+
* the delegate's file-claim pass; this method only collects the set.
|
|
1108
|
+
*/
|
|
1109
|
+
processSassPartImport(node, ctx) {
|
|
1110
|
+
const named = [];
|
|
1111
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1112
|
+
const c = node.namedChild(i);
|
|
1113
|
+
if (c) named.push(c);
|
|
1114
|
+
}
|
|
1115
|
+
if (named.length !== 1 || named[0].type !== "string_value") return;
|
|
1116
|
+
const spec = stripAllQuotes(named[0].text).trim();
|
|
1117
|
+
if (spec.length === 0) return;
|
|
1118
|
+
if (hasCssFileExtension(spec)) return;
|
|
1119
|
+
const lower = spec.toLowerCase();
|
|
1120
|
+
if (lower.startsWith("//") || lower.startsWith("http://") || lower.startsWith("https://")) return;
|
|
1121
|
+
if (lower.startsWith("sass:") || lower.startsWith("data:")) return;
|
|
1122
|
+
ctx.sassPartImports.add(spec);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* MOVE the Sass-form `imports` rows to `part_of`. A LABEL MOVE, NOT A GAIN OR A LOSS: the row's
|
|
1126
|
+
* source, target, line and the delegate's `metadata.importProjectFile` claim are unchanged; only
|
|
1127
|
+
* the type moves, plus the mandatory `partRole` and the structural evidence class. The fixture
|
|
1128
|
+
* suite pins the move in both directions and the census counts the sites, so a reader can check
|
|
1129
|
+
* that nothing vanished.
|
|
1130
|
+
*/
|
|
1131
|
+
relabelSassPartImports(ctx) {
|
|
1132
|
+
if (ctx.sassPartImports.size === 0) return;
|
|
1133
|
+
for (const r of ctx.relationships) {
|
|
1134
|
+
if (r.type !== ASTEngine.RelationshipType.Imports) continue;
|
|
1135
|
+
const target = String(r.target ?? "");
|
|
1136
|
+
if (!ctx.sassPartImports.has(target)) continue;
|
|
1137
|
+
r.type = ASTEngine.RelationshipType.PartOf;
|
|
1138
|
+
r.metadata = {
|
|
1139
|
+
...r.metadata ?? {},
|
|
1140
|
+
// MANDATORY ON EVERY part_of ROW (the vocabulary contract): this file is the LIBRARY —
|
|
1141
|
+
// it carries the directive and names the part it includes.
|
|
1142
|
+
partRole: "library",
|
|
1143
|
+
isSassPartImport: true,
|
|
1144
|
+
evidenceClass: "structural"
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// ── `@return`: a FACT ABOUT A FUNCTION, NOT AN EDGE ─────────────────────────
|
|
1149
|
+
//
|
|
1150
|
+
// There is deliberately no `@return` handling here. The first version emitted
|
|
1151
|
+
// `contains` from the function to ITSELF, which is a self-edge: it makes a
|
|
1152
|
+
// relationship count look like evidence while connecting nothing, and `contains`
|
|
1153
|
+
// between a thing and itself is not a claim any consumer can use.
|
|
1154
|
+
//
|
|
1155
|
+
// A `@return` names no entity. It says the function produces a value, which is a
|
|
1156
|
+
// property of the function, so it is recorded as one — see `scssReturns` on the
|
|
1157
|
+
// function symbol in `processCallable`, which is where it belongs.
|
|
1158
|
+
// ── `@extend` ───────────────────────────────────────────────────────────────
|
|
1159
|
+
processExtend(node, ctx) {
|
|
1160
|
+
this.emitExtend(node, node, node, ctx, false);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* The `@extend` INSIDE A RULE BODY, which is 63 of the 99 `@extend` statements in the
|
|
1164
|
+
* corpus and every one of them was invisible.
|
|
1165
|
+
*
|
|
1166
|
+
* This is a grammar fact and it is worth stating exactly, because it is not a defect
|
|
1167
|
+
* in the analyzer's walk. The `extend_statement` RULE is written for a top-level
|
|
1168
|
+
* `@extend`; nested inside a `block` the same text parses as
|
|
1169
|
+
*
|
|
1170
|
+
* block > ERROR > "@extend", placeholder, ";", ERROR("or", ":", …)
|
|
1171
|
+
*
|
|
1172
|
+
* — so the statement's own node type never appears, the keyword survives only as an
|
|
1173
|
+
* ANONYMOUS token, and the walk (which visits named children) never sees either.
|
|
1174
|
+
* Measured: 125 `@extend` tokens across 4,394 admitted files and only 36
|
|
1175
|
+
* `extend_statement` nodes, so 89 of them — 71% — are in the recovered shape and 63 of
|
|
1176
|
+
* the 99 real `@extend`s (`.card { @extend %badge; }`, which is where a BEM codebase
|
|
1177
|
+
* puts nearly all of them) were producing no edge at all.
|
|
1178
|
+
*
|
|
1179
|
+
* The recognition is by the KEYWORD, not by shape: the first non-whitespace child is
|
|
1180
|
+
* the anonymous token `@extend`. Checking for "an ERROR with a selector in it" would
|
|
1181
|
+
* fire on every broken declaration, and checking node types would check for the type
|
|
1182
|
+
* that is absent.
|
|
1183
|
+
*
|
|
1184
|
+
* The edge is marked `scssRecoveredFromError` because it is read out of a region the
|
|
1185
|
+
* grammar gave up on, and the rest of this lane marks recovered claims the same way
|
|
1186
|
+
* (see `emitVariableReadEdges`). Both ends still resolve: the owner is a rule this
|
|
1187
|
+
* file publishes and the target is a selector this file publishes.
|
|
1188
|
+
*/
|
|
1189
|
+
processRecoveredExtend(node, ctx) {
|
|
1190
|
+
if (!this.isErrorNode(node)) return;
|
|
1191
|
+
let keywordSeen = false;
|
|
1192
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1193
|
+
const c = node.child(i);
|
|
1194
|
+
if (!c) continue;
|
|
1195
|
+
if (c.type === "@extend") {
|
|
1196
|
+
keywordSeen = true;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
if (!c.isNamed || c.type === "comment") continue;
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (!keywordSeen) return;
|
|
1203
|
+
let ruleAncestor = null;
|
|
1204
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
1205
|
+
if (p.type === "rule_set") {
|
|
1206
|
+
ruleAncestor = p;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const ownerNode = ruleAncestor ?? node;
|
|
1211
|
+
const lineNode = node;
|
|
1212
|
+
this.emitExtend(node, ownerNode, lineNode, ctx, true);
|
|
1213
|
+
}
|
|
1214
|
+
emitExtend(targetNode, ownerNode, lineNode, ctx, recovered) {
|
|
1215
|
+
const found = this.extendedSelectorName(targetNode);
|
|
1216
|
+
if (!found) return;
|
|
1217
|
+
const target = found.name;
|
|
1218
|
+
const owner = this.ownerForNode(ownerNode, ctx);
|
|
1219
|
+
if (!owner) return;
|
|
1220
|
+
ctx.relationships.push({
|
|
1221
|
+
source: owner,
|
|
1222
|
+
target,
|
|
1223
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
1224
|
+
line_number: this.lineOf(lineNode),
|
|
1225
|
+
metadata: {
|
|
1226
|
+
evidenceClass: "structural",
|
|
1227
|
+
cssSpecific: true,
|
|
1228
|
+
isScssExtend: true,
|
|
1229
|
+
...recovered ? { scssRecoveredFromError: true, scssExtendShape: "recovered" } : {},
|
|
1230
|
+
// The TARGET was read out of a node the grammar built across a damaged region, not
|
|
1231
|
+
// off the selector the statement names. That is a different claim from the statement
|
|
1232
|
+
// having been recovered, and the lane already marks the statement, so it marks this
|
|
1233
|
+
// too rather than letting a consumer read a repaired edge as a clean one. It is not
|
|
1234
|
+
// a confidence reduction: the referent is a `placeholder` node the parser produced,
|
|
1235
|
+
// which is a parse fact either way, and the contract asks for a confidence consistent
|
|
1236
|
+
// with how the edge was DERIVED, not with how much text was damaged around it.
|
|
1237
|
+
//
|
|
1238
|
+
// It also makes the pre-fix defect reproducible from the RESULT, which is what lets
|
|
1239
|
+
// `scss/extend-target-carries-statement-delimiter` be a faithful mutant. Without the
|
|
1240
|
+
// mark, a mutant keyed on "target carries a delimiter" can never fire — the fixed
|
|
1241
|
+
// analyzer emits none — and a mutant that cannot fire is a green that means nothing.
|
|
1242
|
+
...found.recoveredTarget ? { scssExtendTargetRecoveredFromBrokenNode: true } : {}
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* The selector an `@extend` names, in the spelling the lane publishes selectors.
|
|
1248
|
+
*
|
|
1249
|
+
* Built by SLICING the node's own text rather than by assembling it from node types,
|
|
1250
|
+
* for the reason the CSS lane gives for at-rule preludes: a type list has to be
|
|
1251
|
+
* maintained against the grammar by hand and goes stale the moment the grammar moves.
|
|
1252
|
+
* `.btn` comes out as `.btn`, which is the class symbol the same file publishes.
|
|
1253
|
+
*/
|
|
1254
|
+
extendedSelectorName(node) {
|
|
1255
|
+
const candidates = this.descendants(
|
|
1256
|
+
node,
|
|
1257
|
+
(n) => n.type === "class_selector" || n.type === "id_selector" || n.type === "plain_value" || n.type === "placeholder"
|
|
1258
|
+
);
|
|
1259
|
+
if (candidates.length === 0) return void 0;
|
|
1260
|
+
const sel = candidates[candidates.length - 1];
|
|
1261
|
+
const bound = sel.endIndex;
|
|
1262
|
+
const name = collapseWhitespace(sel.text);
|
|
1263
|
+
let recoveredTarget = false;
|
|
1264
|
+
if (containsStatementDelimiter(name)) {
|
|
1265
|
+
const inner = this.descendants(
|
|
1266
|
+
sel,
|
|
1267
|
+
(n) => n.startIndex < bound && n.type === "placeholder" && isReadablePlaceholder(collapseWhitespace(n.text))
|
|
1268
|
+
);
|
|
1269
|
+
if (inner.length > 0) return { name: collapseWhitespace(inner[0].text), recoveredTarget: true };
|
|
1270
|
+
return void 0;
|
|
1271
|
+
}
|
|
1272
|
+
if (name.startsWith("%") && !isReadablePlaceholder(name)) return void 0;
|
|
1273
|
+
const hasPlaceholder = candidates.some((c) => c.type === "placeholder");
|
|
1274
|
+
const hasClassSelector = candidates.some((c) => c.type === "class_selector" || c.type === "id_selector" || c.type === "plain_value");
|
|
1275
|
+
if (!hasPlaceholder && hasClassSelector && (sel.type === "class_selector" || sel.type === "id_selector" || sel.type === "plain_value")) {
|
|
1276
|
+
const extendKeywordEnd = node.text?.indexOf("@extend") + 7;
|
|
1277
|
+
if (extendKeywordEnd > 0 && sel.startIndex > node.startIndex + extendKeywordEnd + 2) {
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (sel.type === "plain_value" || sel.type === "variable") {
|
|
1282
|
+
for (let p = sel.parent; p && p !== node; p = p.parent) {
|
|
1283
|
+
if (p.type === "interpolation") {
|
|
1284
|
+
return void 0;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (candidates.length > 1) {
|
|
1289
|
+
let shallowest = candidates[0];
|
|
1290
|
+
let minDepth = 0;
|
|
1291
|
+
for (const c of candidates) {
|
|
1292
|
+
let depth = 0;
|
|
1293
|
+
for (let p = c.parent; p && p !== node; p = p.parent) depth++;
|
|
1294
|
+
if (depth < minDepth || minDepth === 0) {
|
|
1295
|
+
minDepth = depth;
|
|
1296
|
+
shallowest = c;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const shallowName = collapseWhitespace(shallowest.text);
|
|
1300
|
+
if (containsStatementDelimiter(shallowName)) recoveredTarget = true;
|
|
1301
|
+
}
|
|
1302
|
+
return name.length > 0 ? { name, recoveredTarget } : void 0;
|
|
1303
|
+
}
|
|
1304
|
+
// ── `@if` / `@each` / `@for` / `@while` ──────────────────────────────────────
|
|
1305
|
+
/**
|
|
1306
|
+
* A control-flow construct is an `interface` — the same shape the CSS lane gives
|
|
1307
|
+
* `@supports`, and for the same reason: it is a CONDITIONAL SCOPE that publishes
|
|
1308
|
+
* symbols and attributes rules to them, not a declaration of a callable.
|
|
1309
|
+
*
|
|
1310
|
+
* The scope is named from the construct's own text, collapsed to one line so a
|
|
1311
|
+
* multi-line `@each` still yields a stable symbol name (M60 R10 — a target must be
|
|
1312
|
+
* spelled the way the lane spells its own symbols, deterministically).
|
|
1313
|
+
*/
|
|
1314
|
+
processControlFlow(node, ctx, owner) {
|
|
1315
|
+
const keyword = node.type.replace("_statement", "");
|
|
1316
|
+
const raw = collapseWhitespace(node.text.split("{")[0] ?? node.text);
|
|
1317
|
+
const atKeyword = `@${keyword}`;
|
|
1318
|
+
const prelude = raw.startsWith(atKeyword) ? raw.slice(atKeyword.length).trim() : raw;
|
|
1319
|
+
const name = `${atKeyword} ${prelude}`;
|
|
1320
|
+
if (name.length <= `@${keyword} `.length) return;
|
|
1321
|
+
this.publishOnce(
|
|
1322
|
+
ctx,
|
|
1323
|
+
{
|
|
1324
|
+
name,
|
|
1325
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1326
|
+
range: this.nodeToRange(node),
|
|
1327
|
+
scope: "",
|
|
1328
|
+
visibility: "public",
|
|
1329
|
+
// one step for the condition, one for each loop/branch variable, one for the body
|
|
1330
|
+
complexity: 1 + this.countDescendantTypes(node, /* @__PURE__ */ new Set(["variable", "block"])),
|
|
1331
|
+
metadata: {
|
|
1332
|
+
isAtRule: true,
|
|
1333
|
+
atKeyword: `@${keyword}`,
|
|
1334
|
+
isScssControlFlow: true,
|
|
1335
|
+
// the loop variables are recorded by name: `@each $k, $v in $map` is the one
|
|
1336
|
+
// place a stylesheet says what it is iterating, and dropping it loses the only
|
|
1337
|
+
// statement of intent the construct makes
|
|
1338
|
+
...node.type === "each_statement" ? {
|
|
1339
|
+
scssEachKey: this.fieldText(node, "key"),
|
|
1340
|
+
scssEachValue: this.fieldText(node, "value")
|
|
1341
|
+
} : {},
|
|
1342
|
+
...node.type === "for_statement" ? { scssForFrom: this.fieldText(node, "from"), scssForThrough: this.fieldText(node, "through") } : {},
|
|
1343
|
+
...node.type === "if_statement" ? { scssCondition: this.fieldText(node, "condition") } : {},
|
|
1344
|
+
...owner ? { scssOwner: owner } : {}
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
// owner AND line: see the note on `publishOnce`
|
|
1348
|
+
`${owner}@${this.lineOf(node) ?? 0}`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
fieldText(node, field) {
|
|
1352
|
+
const c = this.childForField(node, field);
|
|
1353
|
+
return c ? stripAllQuotes(c.text) : void 0;
|
|
1354
|
+
}
|
|
1355
|
+
countDescendantTypes(node, types) {
|
|
1356
|
+
let n = 0;
|
|
1357
|
+
const stack = [node];
|
|
1358
|
+
while (stack.length > 0) {
|
|
1359
|
+
const cur = stack.pop();
|
|
1360
|
+
if (types.has(cur.type)) n++;
|
|
1361
|
+
for (let i = 0; i < cur.namedChildCount; i++) {
|
|
1362
|
+
const c = cur.namedChild(i);
|
|
1363
|
+
if (c) stack.push(c);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return n;
|
|
1367
|
+
}
|
|
1368
|
+
// ── declarations: `$var: value` and `$var` reads ─────────────────────────────
|
|
1369
|
+
/**
|
|
1370
|
+
* A `$var: value` declaration publishes a `variable` symbol, deduplicated by name.
|
|
1371
|
+
*
|
|
1372
|
+
* The CSS lane's identity ruling applies unchanged, and `.scss` is where it matters
|
|
1373
|
+
* most: a Sass variable is assigned in several places by design — a `!default` at the
|
|
1374
|
+
* top and an override inside a theme — and publishing one symbol per assignment is
|
|
1375
|
+
* the identity collision this project has spent the wave removing.
|
|
1376
|
+
*
|
|
1377
|
+
* The FIRST occurrence's range wins, so the symbol points at where the variable is
|
|
1378
|
+
* introduced rather than at its last override.
|
|
1379
|
+
*/
|
|
1380
|
+
processScssDeclaration(node, ctx) {
|
|
1381
|
+
const prop = this.findChild(node, (c) => c.type === "property_name");
|
|
1382
|
+
if (!prop) return;
|
|
1383
|
+
const name = prop.text;
|
|
1384
|
+
if (!name.startsWith("$")) return;
|
|
1385
|
+
if (!ctx.variableNames[name]) ctx.variableNames[name] = name;
|
|
1386
|
+
this.emitGlobalOverride(node, name, ctx);
|
|
1387
|
+
const owner = this.ownerForNode(node, ctx);
|
|
1388
|
+
this.publishOnce(ctx, {
|
|
1389
|
+
name,
|
|
1390
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
1391
|
+
range: this.nodeToRange(node),
|
|
1392
|
+
scope: owner,
|
|
1393
|
+
visibility: "public",
|
|
1394
|
+
complexity: 1 + this.countDescendantTypes(node, /* @__PURE__ */ new Set(["binary_expression", "call_expression", "map", "list"])),
|
|
1395
|
+
metadata: {
|
|
1396
|
+
isScssVariable: true,
|
|
1397
|
+
scssVariableName: name,
|
|
1398
|
+
...owner ? { scssOwner: owner } : {},
|
|
1399
|
+
// `!default` and `!global` are the two flags that change what an assignment
|
|
1400
|
+
// MEANS — a default is overridable, a global assignment leaks out of its
|
|
1401
|
+
// scope — so they are recorded rather than parsed away.
|
|
1402
|
+
...this.hasVisibilityFlag(node, "default") ? { scssFlagDefault: true } : {},
|
|
1403
|
+
...this.hasVisibilityFlag(node, "global") ? { scssFlagGlobal: true } : {}
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Whether a declaration carries `!default` or `!global`.
|
|
1409
|
+
*
|
|
1410
|
+
* The walk uses `childCount`/`child()`, NOT `namedChildCount`/`namedChild()`,
|
|
1411
|
+
* because the flag is TWO ANONYMOUS NODES — the tree is
|
|
1412
|
+
* `declaration > integer_value, "!", "default", ";"` with `!` and `default` both
|
|
1413
|
+
* unnamed. The first version walked named children only, so it could never see
|
|
1414
|
+
* them, the flag was silently always false, and 210 `!default` assignments in the
|
|
1415
|
+
* corpus were recorded as carrying no flag at all.
|
|
1416
|
+
*
|
|
1417
|
+
* Found by `variables-flags-and-identity`, which is the whole argument for writing
|
|
1418
|
+
* a fixture's expectations from the CONTRACT: the analyzer agreed with itself the
|
|
1419
|
+
* entire time, because nothing ever asked.
|
|
1420
|
+
*/
|
|
1421
|
+
hasVisibilityFlag(node, flag) {
|
|
1422
|
+
const stack = [node];
|
|
1423
|
+
while (stack.length > 0) {
|
|
1424
|
+
const n = stack.pop();
|
|
1425
|
+
if (!n.isNamed && n.type === flag) return true;
|
|
1426
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1427
|
+
const c = n.child(i);
|
|
1428
|
+
if (c) stack.push(c);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* A `$var` READ becomes a `references` edge — the same class the CSS lane uses for
|
|
1435
|
+
* `var(--x)`, and the same discipline: only variables THIS FILE declares get an
|
|
1436
|
+
* edge, because a `$var` may arrive as a mixin parameter and inventing an endpoint
|
|
1437
|
+
* for it is the phantom this lane removed once.
|
|
1438
|
+
*
|
|
1439
|
+
* The read is found by DESCENDANT walk from each declaration, so a read inside a
|
|
1440
|
+
* nested value, a `calc()`, or an interpolated selector is found too.
|
|
1441
|
+
*/
|
|
1442
|
+
emitVariableReadEdges(node, ctx, outerRecovered = false) {
|
|
1443
|
+
const NESTED_STATEMENT = /* @__PURE__ */ new Set([
|
|
1444
|
+
"declaration",
|
|
1445
|
+
"block",
|
|
1446
|
+
"rule_set",
|
|
1447
|
+
"mixin_statement",
|
|
1448
|
+
"function_statement",
|
|
1449
|
+
"if_statement",
|
|
1450
|
+
"each_statement",
|
|
1451
|
+
"for_statement",
|
|
1452
|
+
"while_statement",
|
|
1453
|
+
"include_statement",
|
|
1454
|
+
"use_statement",
|
|
1455
|
+
"forward_statement",
|
|
1456
|
+
"return_statement",
|
|
1457
|
+
"extend_statement"
|
|
1458
|
+
]);
|
|
1459
|
+
const stack = [{ node, recovered: outerRecovered }];
|
|
1460
|
+
while (stack.length > 0) {
|
|
1461
|
+
const { node: n, recovered } = stack.pop();
|
|
1462
|
+
if (this.isErrorNode(n)) {
|
|
1463
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1464
|
+
const c = n.namedChild(i);
|
|
1465
|
+
if (c && !NESTED_STATEMENT.has(c.type)) stack.push({ node: c, recovered: true });
|
|
1466
|
+
}
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (n.type === "variable" && ctx.variableNames[n.text]) {
|
|
1470
|
+
const owner = this.ownerForNode(n, ctx);
|
|
1471
|
+
if (!owner) continue;
|
|
1472
|
+
ctx.relationships.push({
|
|
1473
|
+
source: owner,
|
|
1474
|
+
target: n.text,
|
|
1475
|
+
type: ASTEngine.RelationshipType.References,
|
|
1476
|
+
line_number: this.lineOf(n),
|
|
1477
|
+
metadata: {
|
|
1478
|
+
evidenceClass: "usage_contextual",
|
|
1479
|
+
cssSpecific: true,
|
|
1480
|
+
isScssVarRef: true,
|
|
1481
|
+
...recovered ? { scssRecoveredFromError: true } : {}
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
if (n.type === "call_expression") {
|
|
1486
|
+
const fn = this.findChild(n, (c) => c.type === "function_name");
|
|
1487
|
+
const declared = fn ? ctx.functionNames[stripAllQuotes(fn.text)] : void 0;
|
|
1488
|
+
if (fn && declared) {
|
|
1489
|
+
const owner = this.ownerForNode(n, ctx);
|
|
1490
|
+
if (owner) {
|
|
1491
|
+
ctx.relationships.push({
|
|
1492
|
+
source: owner,
|
|
1493
|
+
target: declared,
|
|
1494
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1495
|
+
line_number: this.lineOf(n),
|
|
1496
|
+
metadata: {
|
|
1497
|
+
evidenceClass: "usage_contextual",
|
|
1498
|
+
cssSpecific: true,
|
|
1499
|
+
isScssFunctionCall: true,
|
|
1500
|
+
...recovered ? { scssRecoveredFromError: true } : {}
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1507
|
+
const c = n.namedChild(i);
|
|
1508
|
+
if (c && !NESTED_STATEMENT.has(c.type)) stack.push({ node: c, recovered });
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
export {
|
|
1514
|
+
RealScssAnalyzer
|
|
1515
|
+
};
|