@backbencher/brain 0.0.0-stage → 0.1.1
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-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -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-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -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-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.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,2863 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
4
|
+
|
|
5
|
+
// src/ast/real-css-analyzer.ts
|
|
6
|
+
import { existsSync, statSync } from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
var JS_WHITESPACE = /* @__PURE__ */ new Set([
|
|
9
|
+
" ",
|
|
10
|
+
" ",
|
|
11
|
+
"\n",
|
|
12
|
+
"\v",
|
|
13
|
+
"\f",
|
|
14
|
+
"\r",
|
|
15
|
+
"\xA0",
|
|
16
|
+
"\u1680",
|
|
17
|
+
"\u2002",
|
|
18
|
+
"\u2003",
|
|
19
|
+
"\u2002",
|
|
20
|
+
"\u2003",
|
|
21
|
+
"\u2004",
|
|
22
|
+
"\u2005",
|
|
23
|
+
"\u2006",
|
|
24
|
+
"\u2007",
|
|
25
|
+
"\u2008",
|
|
26
|
+
"\u2009",
|
|
27
|
+
"\u200A",
|
|
28
|
+
"\u2028",
|
|
29
|
+
"\u2029",
|
|
30
|
+
"\u202F",
|
|
31
|
+
"\u205F",
|
|
32
|
+
"\u3000",
|
|
33
|
+
"\uFEFF"
|
|
34
|
+
]);
|
|
35
|
+
function isWhitespaceChar(c) {
|
|
36
|
+
return JS_WHITESPACE.has(c);
|
|
37
|
+
}
|
|
38
|
+
function firstCustomFunctionName(prelude) {
|
|
39
|
+
const trimmed = prelude.trim();
|
|
40
|
+
if (!trimmed.startsWith("--")) return "";
|
|
41
|
+
let end = 2;
|
|
42
|
+
while (end < trimmed.length) {
|
|
43
|
+
const c = trimmed.charAt(end);
|
|
44
|
+
if (!(c === "-" || c === "_" || c >= "0" && c <= "9" || c >= "a" && c <= "z" || c >= "A" && c <= "Z")) break;
|
|
45
|
+
end++;
|
|
46
|
+
}
|
|
47
|
+
return end > 2 ? trimmed.slice(0, end) : "";
|
|
48
|
+
}
|
|
49
|
+
function hasCssFileExtension(specifier) {
|
|
50
|
+
const lower = specifier.toLowerCase();
|
|
51
|
+
let at = lower.indexOf(".css");
|
|
52
|
+
while (at >= 0) {
|
|
53
|
+
const next = lower.charAt(at + 4);
|
|
54
|
+
if (next === "" || next === "?" || next === "#") return true;
|
|
55
|
+
at = lower.indexOf(".css", at + 4);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function stripAllQuotes(text) {
|
|
60
|
+
let out = "";
|
|
61
|
+
for (let i = 0; i < text.length; i++) {
|
|
62
|
+
const c = text[i];
|
|
63
|
+
if (c !== '"' && c !== "'") out += c;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function unwrapUrl(text) {
|
|
68
|
+
if (text.startsWith("url(") && text.endsWith(")")) {
|
|
69
|
+
return stripAllQuotes(text.slice(4, -1).trim());
|
|
70
|
+
}
|
|
71
|
+
return stripAllQuotes(text);
|
|
72
|
+
}
|
|
73
|
+
function replaceAll(text, needle, replacement) {
|
|
74
|
+
let out = "";
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < text.length) {
|
|
77
|
+
if (text.startsWith(needle, i)) {
|
|
78
|
+
out += replacement;
|
|
79
|
+
i += needle.length;
|
|
80
|
+
} else {
|
|
81
|
+
out += text[i];
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function leadingCustomIdent(prelude) {
|
|
88
|
+
const t = prelude.trim();
|
|
89
|
+
if (t.length < 2 || t.charAt(0) !== "-" || t.charAt(1) !== "-") return "";
|
|
90
|
+
let i = 2;
|
|
91
|
+
while (i < t.length && isCustomIdentChar(t.charAt(i))) i++;
|
|
92
|
+
return t.slice(0, i);
|
|
93
|
+
}
|
|
94
|
+
function isCustomIdentChar(c) {
|
|
95
|
+
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "-" || c === "_";
|
|
96
|
+
}
|
|
97
|
+
function stripOccurrenceSuffix(name) {
|
|
98
|
+
let i = name.length;
|
|
99
|
+
while (i > 0) {
|
|
100
|
+
const c = name.charAt(i - 1);
|
|
101
|
+
if (c < "0" || c > "9") break;
|
|
102
|
+
i--;
|
|
103
|
+
}
|
|
104
|
+
if (i === name.length) return name;
|
|
105
|
+
if (i === 0 || name.charAt(i - 1) !== ":") return name;
|
|
106
|
+
return name.slice(0, i - 1);
|
|
107
|
+
}
|
|
108
|
+
function lastPathSegment(p) {
|
|
109
|
+
let cut = -1;
|
|
110
|
+
for (let i = 0; i < p.length; i++) {
|
|
111
|
+
const c = p.charAt(i);
|
|
112
|
+
if (c === "/" || c === "\\") cut = i;
|
|
113
|
+
}
|
|
114
|
+
return cut >= 0 ? p.slice(cut + 1) : p;
|
|
115
|
+
}
|
|
116
|
+
var CSS_PROJECT_ROOT_CACHE = /* @__PURE__ */ new Map();
|
|
117
|
+
function cssProjectRootOf(filePath) {
|
|
118
|
+
const start = path.dirname(path.resolve(filePath));
|
|
119
|
+
const seen = [];
|
|
120
|
+
let dir = start;
|
|
121
|
+
for (let i = 0; i < 64; i++) {
|
|
122
|
+
const cached = CSS_PROJECT_ROOT_CACHE.get(dir);
|
|
123
|
+
if (cached !== void 0) {
|
|
124
|
+
for (const d of seen) CSS_PROJECT_ROOT_CACHE.set(d, cached);
|
|
125
|
+
return cached;
|
|
126
|
+
}
|
|
127
|
+
seen.push(dir);
|
|
128
|
+
if (existsSync(path.join(dir, ".git"))) {
|
|
129
|
+
if (CSS_PROJECT_ROOT_CACHE.size > 2e4) CSS_PROJECT_ROOT_CACHE.clear();
|
|
130
|
+
for (const d of seen) CSS_PROJECT_ROOT_CACHE.set(d, dir);
|
|
131
|
+
return dir;
|
|
132
|
+
}
|
|
133
|
+
const parent = path.dirname(dir);
|
|
134
|
+
if (parent === dir) break;
|
|
135
|
+
dir = parent;
|
|
136
|
+
}
|
|
137
|
+
if (CSS_PROJECT_ROOT_CACHE.size > 2e4) CSS_PROJECT_ROOT_CACHE.clear();
|
|
138
|
+
for (const d of seen) CSS_PROJECT_ROOT_CACHE.set(d, null);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function cssFileScopeOf(filePath) {
|
|
142
|
+
if (!filePath || filePath.length === 0) return "";
|
|
143
|
+
const abs = path.resolve(filePath);
|
|
144
|
+
const root = cssProjectRootOf(abs);
|
|
145
|
+
let rel = root === null ? abs : path.relative(root, abs);
|
|
146
|
+
if (rel.length === 0) rel = abs;
|
|
147
|
+
const dot = rel.lastIndexOf(".");
|
|
148
|
+
const slash = Math.max(rel.lastIndexOf("/"), rel.lastIndexOf("\\"));
|
|
149
|
+
const withoutExt = dot > slash ? rel.slice(0, dot) : rel;
|
|
150
|
+
let out = "";
|
|
151
|
+
for (const ch of withoutExt.split(path.sep).join("/")) out += ch === "." || ch === ":" ? "_" : ch;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function firstToken(text) {
|
|
155
|
+
let i = 0;
|
|
156
|
+
while (i < text.length && isWhitespaceChar(text.charAt(i))) i++;
|
|
157
|
+
const start = i;
|
|
158
|
+
while (i < text.length && !isWhitespaceChar(text.charAt(i))) i++;
|
|
159
|
+
return text.slice(start, i);
|
|
160
|
+
}
|
|
161
|
+
function looksLikeSelectorText(text) {
|
|
162
|
+
for (let i = 0; i < text.length; i++) {
|
|
163
|
+
const c = text[i];
|
|
164
|
+
if (c === ";") return false;
|
|
165
|
+
if (c === "{" && text.charAt(i - 1) !== "#") return false;
|
|
166
|
+
}
|
|
167
|
+
return text.trim().length > 0;
|
|
168
|
+
}
|
|
169
|
+
function collapseWhitespace(text) {
|
|
170
|
+
let out = "";
|
|
171
|
+
let pendingSpace = false;
|
|
172
|
+
for (let i = 0; i < text.length; i++) {
|
|
173
|
+
const c = text[i];
|
|
174
|
+
const isWs = c === " " || c === " " || c === "\n" || c === "\r" || c === "\f";
|
|
175
|
+
if (isWs) {
|
|
176
|
+
if (out.length > 0) pendingSpace = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (pendingSpace) {
|
|
180
|
+
out += " ";
|
|
181
|
+
pendingSpace = false;
|
|
182
|
+
}
|
|
183
|
+
out += c;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function fnNameNameIsVar(fnNameNode) {
|
|
188
|
+
return fnNameNode.text.trim() === "var";
|
|
189
|
+
}
|
|
190
|
+
function hasNestingAmp(text) {
|
|
191
|
+
for (let i = 0; i < text.length; i++) {
|
|
192
|
+
if (text[i] !== "&") continue;
|
|
193
|
+
let bs = 0;
|
|
194
|
+
for (let k = i - 1; k >= 0 && text[k] === "\\"; k--) bs++;
|
|
195
|
+
if (bs % 2 === 0) return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function isResetSelector(text) {
|
|
200
|
+
if (text === "*") return true;
|
|
201
|
+
if (text.startsWith("*::")) {
|
|
202
|
+
const rest = text.slice(3);
|
|
203
|
+
if (rest.length === 0) return false;
|
|
204
|
+
for (let i = 0; i < rest.length; i++) {
|
|
205
|
+
const c = rest[i];
|
|
206
|
+
if (!(c >= "a" && c <= "z" || c === "-")) return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
function isTailwindClass(cls) {
|
|
213
|
+
if (cls.startsWith("tw-")) return true;
|
|
214
|
+
for (const p of ["sm:", "md:", "lg:", "xl:", "2xl:", "hover:", "focus:", "dark:"]) {
|
|
215
|
+
if (cls.startsWith(p)) return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
function isBootstrapClass(cls) {
|
|
220
|
+
if (cls === "container" || cls === "btn-primary" || cls === "navbar-expand") return true;
|
|
221
|
+
for (const size of ["sm-", "md-", "lg-", "xl-"]) {
|
|
222
|
+
if (cls.startsWith(`col-${size}`)) {
|
|
223
|
+
const rest = cls.slice(`col-${size}`.length);
|
|
224
|
+
if (rest.length > 0) {
|
|
225
|
+
let allDigits = true;
|
|
226
|
+
for (let i = 0; i < rest.length; i++) {
|
|
227
|
+
const c = rest[i];
|
|
228
|
+
if (!(c >= "0" && c <= "9")) {
|
|
229
|
+
allDigits = false;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (allDigits) return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
function isBulmaClass(cls) {
|
|
240
|
+
for (const p of ["is-", "has-"]) {
|
|
241
|
+
if (cls.startsWith(p)) {
|
|
242
|
+
const rest = cls.slice(p.length);
|
|
243
|
+
if (rest === "primary" || rest === "info" || rest === "success" || rest === "warning" || rest === "danger" || rest === "text-centered") return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
function isFoundationClass(cls) {
|
|
249
|
+
if (cls === "foundation") return true;
|
|
250
|
+
for (const p of ["small-", "medium-", "large-"]) {
|
|
251
|
+
if (cls.startsWith(p)) {
|
|
252
|
+
const rest = cls.slice(p.length);
|
|
253
|
+
if (rest.length === 0) return false;
|
|
254
|
+
let allDigits = true;
|
|
255
|
+
for (let i = 0; i < rest.length; i++) {
|
|
256
|
+
const c = rest[i];
|
|
257
|
+
if (!(c >= "0" && c <= "9")) {
|
|
258
|
+
allDigits = false;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (allDigits) return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
var RECOVERY_CHILD_CAP = 1e4;
|
|
268
|
+
var STRUCTURAL_SCOPE_AT_RULES = /* @__PURE__ */ new Set(["@layer", "@container", "@scope"]);
|
|
269
|
+
var RealCssAnalyzer = class _RealCssAnalyzer {
|
|
270
|
+
analyze(rootNode, content, filePath, context) {
|
|
271
|
+
const lastSegment = lastPathSegment(filePath);
|
|
272
|
+
this.importerName = lastSegment || filePath || "stylesheet";
|
|
273
|
+
this.claimProjectRoot = typeof context?.projectRoot === "string" ? context.projectRoot : "";
|
|
274
|
+
this.hasRealModuleName = lastSegment.length > 0;
|
|
275
|
+
this.fileScope = cssFileScopeOf(filePath);
|
|
276
|
+
this.fileDir = filePath.length > 0 ? path.dirname(filePath) : "";
|
|
277
|
+
{
|
|
278
|
+
const dot = lastSegment.lastIndexOf(".");
|
|
279
|
+
const ext = dot >= 0 ? lastSegment.slice(dot + 1).toLowerCase() : "";
|
|
280
|
+
this.cssFamilyHost = ext === "css" || ext === "less" || ext === "sass";
|
|
281
|
+
}
|
|
282
|
+
const ctx = {
|
|
283
|
+
symbols: [],
|
|
284
|
+
relationships: [],
|
|
285
|
+
imports: [],
|
|
286
|
+
exports: [],
|
|
287
|
+
errors: [],
|
|
288
|
+
content,
|
|
289
|
+
keyframeNames: {},
|
|
290
|
+
fontFaceNames: {},
|
|
291
|
+
counterStyleNames: {},
|
|
292
|
+
customMediaNames: {},
|
|
293
|
+
cssFunctionNames: {},
|
|
294
|
+
fontFeatureValuesNames: {},
|
|
295
|
+
layerNames: {},
|
|
296
|
+
namespacePrefixes: {},
|
|
297
|
+
layerOrder: [],
|
|
298
|
+
mixinNames: {},
|
|
299
|
+
keyframeLive: {},
|
|
300
|
+
propertyLive: {},
|
|
301
|
+
customPropInBlock: {},
|
|
302
|
+
customPropBlockKey: "",
|
|
303
|
+
overrideEdges: {},
|
|
304
|
+
frameworks: {},
|
|
305
|
+
patterns: {},
|
|
306
|
+
fileHasParseError: rootNode.hasError === true,
|
|
307
|
+
declaredProperties: {},
|
|
308
|
+
mediaScopes: {},
|
|
309
|
+
scopeSymbol: {},
|
|
310
|
+
deferredChainEdges: [],
|
|
311
|
+
selectorChains: {},
|
|
312
|
+
fileSelectorComplexity: 0,
|
|
313
|
+
fileNestingDepth: 0,
|
|
314
|
+
fileMaxSelectorComplexity: 0
|
|
315
|
+
};
|
|
316
|
+
this.collectKeyframeNames(rootNode, ctx);
|
|
317
|
+
this.collectFontFaceNames(rootNode, ctx);
|
|
318
|
+
this.collectCustomDefinitionNames(rootNode, ctx);
|
|
319
|
+
this.collectNamespacePrefixes(rootNode, ctx);
|
|
320
|
+
if (this.hasRealModuleName) {
|
|
321
|
+
ctx.symbols.unshift({
|
|
322
|
+
name: this.importerName,
|
|
323
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
324
|
+
range: this.nodeToRange(rootNode),
|
|
325
|
+
scope: "",
|
|
326
|
+
visibility: "public",
|
|
327
|
+
complexity: 1,
|
|
328
|
+
metadata: {
|
|
329
|
+
cssSpecific: true,
|
|
330
|
+
isStylesheet: true,
|
|
331
|
+
stylesheetPath: filePath
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const stack = [{ node: rootNode, media: "", supports: "", layer: "", container: "", scope: "", currentSelector: "", keyframeScope: "" }];
|
|
336
|
+
while (stack.length > 0) {
|
|
337
|
+
const { node, media, supports, layer, container, scope, currentSelector, keyframeScope } = stack.pop();
|
|
338
|
+
if (this.isErrorNode(node)) {
|
|
339
|
+
ctx.errors.push({
|
|
340
|
+
message: `Syntax ERROR: the grammar could not read this token.`,
|
|
341
|
+
range: this.nodeToRange(node),
|
|
342
|
+
severity: `warning`
|
|
343
|
+
});
|
|
344
|
+
} else if (node.hasError) {
|
|
345
|
+
ctx.errors.push({
|
|
346
|
+
message: `Syntax ERROR detected inside this rule; the rule itself is extracted.`,
|
|
347
|
+
range: this.nodeToRange(node),
|
|
348
|
+
severity: `warning`
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
let nextMedia = media;
|
|
352
|
+
let nextSupports = supports;
|
|
353
|
+
let nextLayer = layer;
|
|
354
|
+
let nextContainer = container;
|
|
355
|
+
let nextScope = scope;
|
|
356
|
+
let nextSelector = currentSelector;
|
|
357
|
+
let nextKeyframeScope = keyframeScope;
|
|
358
|
+
switch (node.type) {
|
|
359
|
+
case "rule_set":
|
|
360
|
+
nextSelector = this.processRuleSet(node, media, supports, layer, container, scope, currentSelector, ctx) || nextSelector;
|
|
361
|
+
break;
|
|
362
|
+
case "import_statement":
|
|
363
|
+
this.processImport(node, ctx);
|
|
364
|
+
break;
|
|
365
|
+
case "media_statement": {
|
|
366
|
+
const newMedia = this.processMedia(node, ctx);
|
|
367
|
+
if (newMedia) {
|
|
368
|
+
nextMedia = media ? `${media} and ${newMedia.replace("@media ", "")}` : newMedia;
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case "keyframes_statement": {
|
|
373
|
+
const kfName = this.processKeyframes(node, ctx);
|
|
374
|
+
nextKeyframeScope = kfName;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case "supports_statement": {
|
|
378
|
+
const newSupports = this.processSupports(node, ctx);
|
|
379
|
+
if (newSupports) nextSupports = newSupports;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "at_rule":
|
|
383
|
+
case "postcss_statement":
|
|
384
|
+
case "namespace_statement":
|
|
385
|
+
case "scope_statement":
|
|
386
|
+
case "charset_statement": {
|
|
387
|
+
const scopeUpdate = this.processAtRule(node, currentSelector, ctx);
|
|
388
|
+
if (scopeUpdate.layer) nextLayer = scopeUpdate.layer;
|
|
389
|
+
if (scopeUpdate.container) nextContainer = scopeUpdate.container;
|
|
390
|
+
if (scopeUpdate.scope) nextScope = scopeUpdate.scope;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "declaration": {
|
|
394
|
+
this.processDeclaration(node, currentSelector, ctx);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "call_expression":
|
|
398
|
+
this.processCallExpression(node, currentSelector, keyframeScope, ctx);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const wide = node.namedChildCount > RECOVERY_CHILD_CAP;
|
|
402
|
+
const first = wide ? RECOVERY_CHILD_CAP : node.namedChildCount;
|
|
403
|
+
if (wide) {
|
|
404
|
+
const tally = ctx.recoveryCap ??= { cap: RECOVERY_CHILD_CAP, errorNodes: 0, errorChildren: 0, visited: 0, skipped: 0 };
|
|
405
|
+
tally.errorNodes++;
|
|
406
|
+
tally.errorChildren += node.namedChildCount;
|
|
407
|
+
tally.visited += RECOVERY_CHILD_CAP;
|
|
408
|
+
tally.skipped += node.namedChildCount - RECOVERY_CHILD_CAP;
|
|
409
|
+
}
|
|
410
|
+
for (let i = first - 1; i >= 0; i--) {
|
|
411
|
+
const child = node.namedChild(i);
|
|
412
|
+
if (child) {
|
|
413
|
+
stack.push({
|
|
414
|
+
node: child,
|
|
415
|
+
media: nextMedia,
|
|
416
|
+
supports: nextSupports,
|
|
417
|
+
layer: nextLayer,
|
|
418
|
+
container: nextContainer,
|
|
419
|
+
scope: nextScope,
|
|
420
|
+
currentSelector: nextSelector,
|
|
421
|
+
keyframeScope: nextKeyframeScope
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (ctx.recoveryCap && ctx.relationships.length > 0) {
|
|
427
|
+
ctx.relationships[0].metadata = { ...ctx.relationships[0].metadata ?? {}, cssRecoveryCap: ctx.recoveryCap };
|
|
428
|
+
}
|
|
429
|
+
{
|
|
430
|
+
const names = /* @__PURE__ */ new Set();
|
|
431
|
+
for (const sym of ctx.symbols) {
|
|
432
|
+
names.add(sym.name);
|
|
433
|
+
if (sym.scope) names.add(`${sym.scope}.${sym.name}`);
|
|
434
|
+
}
|
|
435
|
+
const chainTermKind = (n) => {
|
|
436
|
+
if (n.length === 0) return "";
|
|
437
|
+
const c = n.charAt(0);
|
|
438
|
+
if (c === ".") return "class";
|
|
439
|
+
if (c === "#") return "id";
|
|
440
|
+
if (c === ":") return "pseudo";
|
|
441
|
+
if (c === "*") return "universal";
|
|
442
|
+
return "bare";
|
|
443
|
+
};
|
|
444
|
+
const leafOf = (n) => {
|
|
445
|
+
const segs = stripOccurrenceSuffix(n).split(":").filter((x) => x.length > 0);
|
|
446
|
+
const last = segs[segs.length - 1] ?? "";
|
|
447
|
+
const dots = last.split(".");
|
|
448
|
+
return dots[dots.length - 1];
|
|
449
|
+
};
|
|
450
|
+
const resolves = (n) => {
|
|
451
|
+
if (!n) return false;
|
|
452
|
+
if (names.has(n)) return true;
|
|
453
|
+
const kind = chainTermKind(n);
|
|
454
|
+
const l = leafOf(n);
|
|
455
|
+
if (l !== n && chainTermKind(l) === kind && names.has(l)) return true;
|
|
456
|
+
for (let i = 0; i < n.length; i++) {
|
|
457
|
+
const c = n[i];
|
|
458
|
+
if (c !== ":" && c !== ".") continue;
|
|
459
|
+
const tail = n.slice(i + 1);
|
|
460
|
+
if (tail.length > 0 && chainTermKind(tail) === kind && names.has(tail)) return true;
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
};
|
|
464
|
+
let emitted = 0;
|
|
465
|
+
let withheld = 0;
|
|
466
|
+
const seen = /* @__PURE__ */ new Set();
|
|
467
|
+
for (const e of ctx.deferredChainEdges) {
|
|
468
|
+
const key = `${e.from}${e.to}`;
|
|
469
|
+
if (seen.has(key)) continue;
|
|
470
|
+
seen.add(key);
|
|
471
|
+
if (resolves(e.from) && resolves(e.to)) {
|
|
472
|
+
emitted++;
|
|
473
|
+
ctx.relationships.push({
|
|
474
|
+
source: e.from,
|
|
475
|
+
target: e.to,
|
|
476
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
477
|
+
line_number: e.line,
|
|
478
|
+
metadata: { cssSpecific: true, isSelectorChain: true }
|
|
479
|
+
});
|
|
480
|
+
} else {
|
|
481
|
+
withheld++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
ctx.chainEdgeTally = { emitted, withheld };
|
|
485
|
+
}
|
|
486
|
+
const attachDeclared = (sym) => {
|
|
487
|
+
const direct = ctx.declaredProperties[sym.name];
|
|
488
|
+
const parts = sym.name.split(",").map((x) => x.trim()).filter(Boolean);
|
|
489
|
+
const merged = [];
|
|
490
|
+
const push = (list) => {
|
|
491
|
+
if (!list) return;
|
|
492
|
+
for (const p of list) if (merged.indexOf(p) === -1) merged.push(p);
|
|
493
|
+
};
|
|
494
|
+
push(direct);
|
|
495
|
+
for (const part of parts) push(ctx.declaredProperties[part]);
|
|
496
|
+
if (merged.length === 0) return;
|
|
497
|
+
if (!sym.metadata) sym.metadata = {};
|
|
498
|
+
sym.metadata.declaresProperties = merged;
|
|
499
|
+
sym.metadata.declaredPropertyCount = merged.length;
|
|
500
|
+
};
|
|
501
|
+
const attachChain = (sym) => {
|
|
502
|
+
const direct = ctx.selectorChains[sym.name];
|
|
503
|
+
const parts = sym.name.split(",").map((x) => x.trim()).filter(Boolean);
|
|
504
|
+
const merged = [];
|
|
505
|
+
const push = (list) => {
|
|
506
|
+
if (!list) return;
|
|
507
|
+
for (const p of list) if (merged.indexOf(p) === -1) merged.push(p);
|
|
508
|
+
};
|
|
509
|
+
push(direct);
|
|
510
|
+
for (const part of parts) push(ctx.selectorChains[part]);
|
|
511
|
+
if (merged.length < 2) return;
|
|
512
|
+
if (!sym.metadata) sym.metadata = {};
|
|
513
|
+
sym.metadata.selectorChain = merged;
|
|
514
|
+
};
|
|
515
|
+
{
|
|
516
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
517
|
+
const merged = [];
|
|
518
|
+
for (const sym of ctx.symbols) {
|
|
519
|
+
const key = `${sym.kind}\0${sym.name}`;
|
|
520
|
+
const first = byIdentity.get(key);
|
|
521
|
+
if (!first) {
|
|
522
|
+
byIdentity.set(key, sym);
|
|
523
|
+
merged.push(sym);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const a = first.metadata ?? {};
|
|
527
|
+
const b = sym.metadata ?? {};
|
|
528
|
+
const out = { ...a };
|
|
529
|
+
for (const [k, v] of Object.entries(b)) {
|
|
530
|
+
const cur = out[k];
|
|
531
|
+
if (Array.isArray(v) && Array.isArray(cur)) {
|
|
532
|
+
for (const item of v) if (!cur.includes(item)) cur.push(item);
|
|
533
|
+
} else if (typeof v === "boolean" && typeof cur === "boolean") {
|
|
534
|
+
out[k] = cur || v;
|
|
535
|
+
} else if (typeof v === "number" && typeof cur === "number" && (k.includes("specificity") || k.includes("omplexity"))) {
|
|
536
|
+
out[k] = Math.max(cur, v);
|
|
537
|
+
} else if (cur === void 0) {
|
|
538
|
+
out[k] = v;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
first.metadata = out;
|
|
542
|
+
const firstLine = first.range?.startLine ?? 0;
|
|
543
|
+
const lastLine = sym.range?.endLine ?? firstLine;
|
|
544
|
+
if (lastLine > (first.range?.endLine ?? 0)) {
|
|
545
|
+
first.range = { ...first.range, endLine: lastLine };
|
|
546
|
+
}
|
|
547
|
+
const occ = first.metadata.occurrences ?? 1;
|
|
548
|
+
first.metadata.occurrences = occ + 1;
|
|
549
|
+
const prevScopes = first.metadata.scopes ?? (first.scope ? [first.scope] : []);
|
|
550
|
+
if (sym.scope && !prevScopes.includes(sym.scope)) prevScopes.push(sym.scope);
|
|
551
|
+
if (prevScopes.length > 1) first.metadata.scopes = prevScopes;
|
|
552
|
+
if (firstLine > 0) first.metadata.firstLine = Math.min(first.metadata.firstLine ?? firstLine, firstLine);
|
|
553
|
+
}
|
|
554
|
+
ctx.symbols = merged;
|
|
555
|
+
}
|
|
556
|
+
for (const s2 of ctx.symbols) attachChain(s2);
|
|
557
|
+
for (const s of ctx.symbols) attachDeclared(s);
|
|
558
|
+
for (const s of ctx.symbols) {
|
|
559
|
+
if (!s.scope || s.scope === "") {
|
|
560
|
+
ctx.exports.push({ name: s.name, kind: s.kind, range: s.range, visibility: s.visibility || "public" });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
this.detectFrameworksFromClasses(ctx);
|
|
564
|
+
const frameworks = [];
|
|
565
|
+
for (const f in ctx.frameworks) frameworks.push(f);
|
|
566
|
+
const patterns = [];
|
|
567
|
+
for (const p in ctx.patterns) patterns.push(p);
|
|
568
|
+
const metrics = this.calculateMetrics(ctx);
|
|
569
|
+
const frameworkPatterns = [];
|
|
570
|
+
for (let i = 0; i < frameworks.length; i++) {
|
|
571
|
+
frameworkPatterns.push({
|
|
572
|
+
framework: frameworks[i],
|
|
573
|
+
patterns: [{ type: "usage", detected: true, description: `${frameworks[i]} usage detected` }]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const evidenceClassFor = (type) => {
|
|
577
|
+
if (type === ASTEngine.RelationshipType.Imports || type === ASTEngine.RelationshipType.References) return "import_based";
|
|
578
|
+
if (type === ASTEngine.RelationshipType.Uses) return "usage_contextual";
|
|
579
|
+
return "structural";
|
|
580
|
+
};
|
|
581
|
+
let unrankable = 0;
|
|
582
|
+
for (const rel of ctx.relationships) {
|
|
583
|
+
const md = rel.metadata ?? {};
|
|
584
|
+
if (typeof md.evidenceClass === "string" && md.evidenceClass.length > 0) continue;
|
|
585
|
+
if (rel.type == null) {
|
|
586
|
+
unrankable++;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
rel.metadata = { evidenceClass: evidenceClassFor(rel.type), ...md };
|
|
590
|
+
}
|
|
591
|
+
this.publishFileScopedIdentities(ctx);
|
|
592
|
+
return {
|
|
593
|
+
language: "css",
|
|
594
|
+
symbols: ctx.symbols,
|
|
595
|
+
relationships: ctx.relationships,
|
|
596
|
+
imports: ctx.imports,
|
|
597
|
+
exports: ctx.exports,
|
|
598
|
+
errors: ctx.errors,
|
|
599
|
+
metadata: {
|
|
600
|
+
complexity: ctx.symbols.length,
|
|
601
|
+
linesOfCode: content.split("\n").length,
|
|
602
|
+
// NB: the cascade order is NOT recorded here. `parseFile` rebuilds file
|
|
603
|
+
// metadata as a fresh literal of specific keys (engine.ts:1572 — its own
|
|
604
|
+
// comment says an omitted key is "computed and DISCARDED — silently"), so a
|
|
605
|
+
// key added at the analyzer would never reach a consumer, and a key that
|
|
606
|
+
// looks present but is always dropped is worse than an absent one. The order
|
|
607
|
+
// rides each layer SYMBOL as `layerIndex`, which is passed through.
|
|
608
|
+
// 0 on every real corpus file; present so a non-zero value is a number
|
|
609
|
+
// someone can go and look at rather than a branch nobody can observe.
|
|
610
|
+
cssUnrankedRelationships: unrankable,
|
|
611
|
+
// M60 R6 / CC1. This was a literal 1, and because
|
|
612
|
+
// `pool-worker.ts` computes `complexity: s.complexity ||
|
|
613
|
+
// s.metadata?.complexity || 1`, every CSS symbol in the store claimed a
|
|
614
|
+
// complexity of exactly 1 — measured 9,338 of 9,338, 100%. A CC1 check
|
|
615
|
+
// that asserts `complexity >= 1` is therefore satisfied vacuously for
|
|
616
|
+
// this lane: it cannot distinguish a one-line reset from a deeply nested
|
|
617
|
+
// selector, because both answer 1.
|
|
618
|
+
//
|
|
619
|
+
// The number is now MEASURED, and it is the thing a CSS author actually
|
|
620
|
+
// finds hard: how much selector machinery a single rule carries. See
|
|
621
|
+
// `selectorComplexity` for the terms and why each is there.
|
|
622
|
+
cyclomaticComplexity: metrics.complexity,
|
|
623
|
+
framework: frameworks[0] || "Standard CSS",
|
|
624
|
+
patterns,
|
|
625
|
+
detectedFrameworks: frameworks,
|
|
626
|
+
frameworkPatterns,
|
|
627
|
+
architecturalInsights: this.generateInsights(ctx, frameworks),
|
|
628
|
+
architecturalScore: metrics.score,
|
|
629
|
+
codeQuality: metrics.quality,
|
|
630
|
+
hasTypeInference: false,
|
|
631
|
+
hasFrameworkIntelligence: true,
|
|
632
|
+
maintainabilityScore: metrics.maintainability,
|
|
633
|
+
performanceScore: metrics.performance,
|
|
634
|
+
analysisVersion: "8.0.0-infallible-brain-math-strict"
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* CSS-D15 — publish a project-unique identity per declaration, and respell the edges whose
|
|
640
|
+
* declaration is in the edge's own file. See the FILE SCOPE note above `cssFileScopeOf` for
|
|
641
|
+
* the measurement this answers and why the spelling is `<dotless-scope>::<name>`.
|
|
642
|
+
*
|
|
643
|
+
* TWO RULES, and both are deliberate:
|
|
644
|
+
*
|
|
645
|
+
* 1. A target is respelled ONLY when this file publishes exactly one symbol with that name.
|
|
646
|
+
* Then the edge's own file is the whole answer and the spelling is exact. The fallback is
|
|
647
|
+
* the bare name, which is what the lane emitted before this change — so this pass can only
|
|
648
|
+
* ADD resolution, never remove it (the `json` lane's rule for the same fix).
|
|
649
|
+
*
|
|
650
|
+
* 2. Where this file publishes the name 2+ times (the gate's `IN-FILE-MULTI`) the edge does
|
|
651
|
+
* not say which site it means, so the target is left BARE and stays visibly ambiguous. The
|
|
652
|
+
* occurrence discriminator exists only to keep those two symbols' `qualifiedName`s
|
|
653
|
+
* distinct, so a direct reference to one of them is still resolvable.
|
|
654
|
+
*
|
|
655
|
+
* NO TARGET IS REWRITTEN ON AN IMPORT: an `imports` edge names ANOTHER file's module, and this
|
|
656
|
+
* file's module symbol happens to share its spelling with that file's basename — respelling it
|
|
657
|
+
* would point the import at the importing file itself.
|
|
658
|
+
*/
|
|
659
|
+
publishFileScopedIdentities(ctx) {
|
|
660
|
+
const byName = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const sym of ctx.symbols) {
|
|
662
|
+
const name = String(sym.name ?? "");
|
|
663
|
+
if (name.length === 0) continue;
|
|
664
|
+
const existing = byName.get(name);
|
|
665
|
+
const occurrence = existing === void 0 ? 0 : existing.length;
|
|
666
|
+
const prefix = this.fileScope;
|
|
667
|
+
const qname = occurrence === 0 ? prefix === "" ? name : `${prefix}::${name}` : `${prefix}#${occurrence + 1}::${name}`;
|
|
668
|
+
sym.qualifiedName = qname;
|
|
669
|
+
if (existing === void 0) byName.set(name, [{ sym, qname }]);
|
|
670
|
+
else existing.push({ sym, qname });
|
|
671
|
+
}
|
|
672
|
+
if (this.fileScope === "") return;
|
|
673
|
+
const REWRITABLE = /* @__PURE__ */ new Set([
|
|
674
|
+
ASTEngine.RelationshipType.Contains,
|
|
675
|
+
ASTEngine.RelationshipType.References,
|
|
676
|
+
ASTEngine.RelationshipType.Uses,
|
|
677
|
+
ASTEngine.RelationshipType.Overrides
|
|
678
|
+
]);
|
|
679
|
+
for (const rel of ctx.relationships) {
|
|
680
|
+
const type = String(rel.type ?? "");
|
|
681
|
+
if (!REWRITABLE.has(type)) continue;
|
|
682
|
+
const target = String(rel.target ?? "");
|
|
683
|
+
if (target.length === 0) continue;
|
|
684
|
+
const candidates = byName.get(target);
|
|
685
|
+
if (candidates === void 0 || candidates.length === 0) continue;
|
|
686
|
+
let pick = candidates[0];
|
|
687
|
+
if (candidates.length > 1) {
|
|
688
|
+
const md = rel.metadata ?? {};
|
|
689
|
+
if (md.isAnimationRef === true) {
|
|
690
|
+
const keyframes = candidates.filter((c) => c.sym.metadata?.isKeyframes === true);
|
|
691
|
+
if (keyframes.length !== 1) continue;
|
|
692
|
+
pick = keyframes[0];
|
|
693
|
+
} else {
|
|
694
|
+
const line = typeof rel.line_number === "number" ? rel.line_number : null;
|
|
695
|
+
if (line === null) continue;
|
|
696
|
+
const covering = candidates.filter((c) => {
|
|
697
|
+
const r = c.sym.range;
|
|
698
|
+
return r !== void 0 && line >= r.startLine && line <= r.endLine;
|
|
699
|
+
});
|
|
700
|
+
if (covering.length !== 1) continue;
|
|
701
|
+
pick = covering[0];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
rel.target = pick.qname;
|
|
705
|
+
}
|
|
706
|
+
let marked = 0;
|
|
707
|
+
for (const rel of ctx.relationships) {
|
|
708
|
+
if (String(rel.type ?? "") !== ASTEngine.RelationshipType.References) continue;
|
|
709
|
+
const target = String(rel.target ?? "");
|
|
710
|
+
if (!target.startsWith("--") || target.includes(".")) continue;
|
|
711
|
+
if (byName.has(target)) continue;
|
|
712
|
+
rel.metadata = { ...rel.metadata ?? {}, isExternalTarget: true };
|
|
713
|
+
marked++;
|
|
714
|
+
}
|
|
715
|
+
ctx.externalCustomPropertyReads = marked;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* True when the node IS the parse failure, rather than merely containing one.
|
|
719
|
+
*
|
|
720
|
+
* `Node.hasError` is true for a node with a broken descendant too, so using it
|
|
721
|
+
* as a suppression test would discard declarations that are well formed. A
|
|
722
|
+
* MISSING node is a token the parser inserted where the source had none,
|
|
723
|
+
* which is equally not a declaration.
|
|
724
|
+
*/
|
|
725
|
+
/**
|
|
726
|
+
* The 1-based line of a node — the convention, matching the symbol
|
|
727
|
+
* `line_start` base the LSP reads with `line - 1`.
|
|
728
|
+
*
|
|
729
|
+
* / #825. The rule this lane follows is one sentence: **the line belongs
|
|
730
|
+
* to the NAME, never to the construct that contains it.** Every call site below
|
|
731
|
+
* passes the node the NAME is written on, not the node that encloses it:
|
|
732
|
+
*
|
|
733
|
+
* a `contains` scope edge -> the rule's `selectors` node, not the
|
|
734
|
+
* `rule_set` (which opens on the same line
|
|
735
|
+
* here, but not for a nested rule under a
|
|
736
|
+
* multi-line selector)
|
|
737
|
+
* a custom-property `contains` -> the `property_name` node, not the
|
|
738
|
+
* `declaration` (a declaration can carry a
|
|
739
|
+
* leading comment or a newline before the
|
|
740
|
+
* name)
|
|
741
|
+
* an `animation` reference -> the VALUE node naming the keyframes, not
|
|
742
|
+
* the `animation` property (the referenced
|
|
743
|
+
* name is written on the value's line, and a
|
|
744
|
+
* shorthand puts several there)
|
|
745
|
+
* a `var()` reference -> the ARGUMENT node, not the
|
|
746
|
+
* `call_expression` (whose start is the
|
|
747
|
+
* receiver's position)
|
|
748
|
+
* a `composes` edge -> the composed name's value node
|
|
749
|
+
* an `imports` edge -> the module NAME node, not the `@import`
|
|
750
|
+
* keyword (the target is written after it,
|
|
751
|
+
* and can be on a later line)
|
|
752
|
+
* a mixin reference -> the `@include` at-rule's own line
|
|
753
|
+
* a selector-chain edge -> the `selectors` node the chain is read
|
|
754
|
+
* from
|
|
755
|
+
*
|
|
756
|
+
* Where a line genuinely cannot be derived the field is ABSENT, never 0 and
|
|
757
|
+
* never the enclosing symbol's line: `absent` is the honest value and 0 is a
|
|
758
|
+
* lie a reader cannot detect. There are no such cases in this lane — every
|
|
759
|
+
* relationship CSS emits is between two names that are written in the file —
|
|
760
|
+
* and the exceptions list in docs/analyzers/css.md records that
|
|
761
|
+
* as a measured fact rather than an assumption.
|
|
762
|
+
*/
|
|
763
|
+
lineOf(node) {
|
|
764
|
+
if (!node) return void 0;
|
|
765
|
+
return node.startPosition.row + 1;
|
|
766
|
+
}
|
|
767
|
+
isErrorNode(node) {
|
|
768
|
+
return node.type === "ERROR" || node.isMissing === true;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* True when a `selectors` node can only be error-recovery debris.
|
|
772
|
+
*
|
|
773
|
+
* A stylesheet-level selector is never a bare string. The one way a `rule_set`
|
|
774
|
+
* gets a `string_value` in selector position is the grammar abandoning a
|
|
775
|
+
* construct's head (emitting an ERROR for it) and re-parsing the remainder as
|
|
776
|
+
* a sibling rule — measured on `@keyframes "fade" { … }`, whose fabricated
|
|
777
|
+
* output was three symbols whose names carried literal quote characters.
|
|
778
|
+
*
|
|
779
|
+
* The `selectors` node is examined for ANY real selector form; if it has one,
|
|
780
|
+
* this is a genuine rule and nothing is suppressed. A genuine CSS selector can
|
|
781
|
+
* contain a string only inside `[attr="value"]`, and there the string sits
|
|
782
|
+
* under an `attribute_selector`, never as a selector of its own.
|
|
783
|
+
*/
|
|
784
|
+
/**
|
|
785
|
+
* The leading `@keyword` of an at-rule's own text, or `""`.
|
|
786
|
+
*
|
|
787
|
+
* Pure-String, bounded to the characters CSS actually allows in an at-keyword
|
|
788
|
+
* (`@`, then letters/digits/`-`/`_`), so it cannot run past the keyword into a
|
|
789
|
+
* prelude. Returns `""` when the text does not start with `@`, which is the
|
|
790
|
+
* honest answer for a node that is not an at-rule at all.
|
|
791
|
+
*/
|
|
792
|
+
leadingAtKeyword(text) {
|
|
793
|
+
if (text.charAt(0) !== "@") return "";
|
|
794
|
+
let i = 1;
|
|
795
|
+
while (i < text.length) {
|
|
796
|
+
const c = text[i];
|
|
797
|
+
const ok = c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "-" || c === "_";
|
|
798
|
+
if (!ok) break;
|
|
799
|
+
i++;
|
|
800
|
+
}
|
|
801
|
+
return i > 1 ? text.slice(0, i) : "";
|
|
802
|
+
}
|
|
803
|
+
/** First descendant satisfying `predicate`, breadth-first. */
|
|
804
|
+
findDescendant(node, predicate) {
|
|
805
|
+
const queue = [node];
|
|
806
|
+
while (queue.length > 0) {
|
|
807
|
+
const n = queue.shift();
|
|
808
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
809
|
+
const c = n.namedChild(i);
|
|
810
|
+
if (!c) continue;
|
|
811
|
+
if (predicate(c)) return c;
|
|
812
|
+
queue.push(c);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
isKeyframeStepSelector(selectorsNode) {
|
|
818
|
+
const t = selectorsNode.text.trim();
|
|
819
|
+
return t === "from" || t === "to";
|
|
820
|
+
}
|
|
821
|
+
isRecoveryDebris(selectorsNode) {
|
|
822
|
+
let hasString = false;
|
|
823
|
+
let hasRealSelector = false;
|
|
824
|
+
for (let i = 0; i < selectorsNode.namedChildCount; i++) {
|
|
825
|
+
const c = selectorsNode.namedChild(i);
|
|
826
|
+
if (!c) continue;
|
|
827
|
+
const t = c.type;
|
|
828
|
+
if (t === "string_value") {
|
|
829
|
+
hasString = true;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (t === "class_selector" || t === "id_selector" || t === "tag_name" || t === "universal_selector" || t === "pseudo_class_selector" || t === "pseudo_element_selector" || t === "attribute_selector" || t === "nesting_selector" || t === "namespace_selector" || t === "child_selector" || t === "descendant_selector" || t === "sibling_selector" || t === "adjacent_sibling_selector") {
|
|
833
|
+
hasRealSelector = true;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
hasRealSelector = true;
|
|
837
|
+
}
|
|
838
|
+
return hasString && !hasRealSelector;
|
|
839
|
+
}
|
|
840
|
+
/** The importing stylesheet's name; set per `analyze` run. */
|
|
841
|
+
importerName = "stylesheet";
|
|
842
|
+
/** Whether `importerName` is a real basename rather than the fallback literal. */
|
|
843
|
+
hasRealModuleName = false;
|
|
844
|
+
/** The project-unique scope prefix for the file being analyzed; set per `analyze` run. */
|
|
845
|
+
fileScope = "";
|
|
846
|
+
/** The directory of the file being analyzed, for resolving a RELATIVE import specifier. */
|
|
847
|
+
fileDir = "";
|
|
848
|
+
/**
|
|
849
|
+
* The CALLER's project root, or `""`. Set per `analyze` run from `context.projectRoot`.
|
|
850
|
+
*
|
|
851
|
+
* Used for exactly one thing: turning the sibling file an `@import` resolves to into the
|
|
852
|
+
* PROJECT-RELATIVE path an `importProjectFile` claim carries. Empty (the fixture runner's
|
|
853
|
+
* case) means the claim is not stated — see the note at the assignment in `analyze`.
|
|
854
|
+
*/
|
|
855
|
+
claimProjectRoot = "";
|
|
856
|
+
// THE HOST-EXTENSION GATE: `part_of` is enabled only for the extensions this lane admits
|
|
857
|
+
// (`css,less,sass`). scss delegates its walk here and relabels the Sass form ITSELF, and html
|
|
858
|
+
// delegates with an extensionless module name — so the relabel must be decided by the file this
|
|
859
|
+
// run is analyzing, not by a flag a caller could forget. Computed once in analyze().
|
|
860
|
+
cssFamilyHost = false;
|
|
861
|
+
determineSelectorKind(hasId, hasClass, hasTag, hasPseudoClass, hasPseudoElement, hasAttribute, isComplex) {
|
|
862
|
+
if (hasPseudoElement && !hasClass && !hasId && !hasTag) return ASTEngine.SymbolKind.Property;
|
|
863
|
+
if (hasPseudoClass && !hasClass && !hasId && !hasTag && !hasAttribute) return ASTEngine.SymbolKind.Property;
|
|
864
|
+
if (hasId && !hasClass && !hasTag && !hasPseudoClass && !hasPseudoElement && !hasAttribute)
|
|
865
|
+
return ASTEngine.SymbolKind.Interface;
|
|
866
|
+
if (hasTag && !hasClass && !hasId && !hasPseudoClass && !hasPseudoElement && !hasAttribute)
|
|
867
|
+
return ASTEngine.SymbolKind.Type;
|
|
868
|
+
if (hasAttribute && !hasClass && !hasId && !hasTag && !hasPseudoClass && !hasPseudoElement)
|
|
869
|
+
return ASTEngine.SymbolKind.Property;
|
|
870
|
+
if (hasClass && !hasId && !hasTag && !hasPseudoClass && !hasPseudoElement && !hasAttribute && !isComplex)
|
|
871
|
+
return ASTEngine.SymbolKind.Class;
|
|
872
|
+
return ASTEngine.SymbolKind.Class;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Exactly the code points JS `\s` matches, as a set.
|
|
876
|
+
*
|
|
877
|
+
* This exists so the whitespace tests in `queryComplexity` are not regexes. The
|
|
878
|
+
* M12 regex census drives that count DOWN, and five of the nine regexes this lane
|
|
879
|
+
* carried were added by the complexity work two commits ago — `queryComplexity`
|
|
880
|
+
* testing `/\s/` once per CHARACTER of every query, which is both a census
|
|
881
|
+
* regression and slower than a set lookup. The full set is listed rather than the
|
|
882
|
+
* ASCII five so the replacement is not a NARROWING: `\s` also matches \v, NBSP,
|
|
883
|
+
* the U+2000 block, the line/paragraph separators and the BOM, and a predicate
|
|
884
|
+
* that quietly stops matching a code point is a behaviour change wearing a
|
|
885
|
+
* cleanup's commit message.
|
|
886
|
+
*/
|
|
887
|
+
static JS_SPACE = JS_WHITESPACE;
|
|
888
|
+
isSpace(c) {
|
|
889
|
+
return c !== void 0 && _RealCssAnalyzer.JS_SPACE.has(c);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Complexity of a media/supports/container QUERY, in the same spirit as the
|
|
893
|
+
* selector metric: 1, plus one for each boolean operator that joins or negates a
|
|
894
|
+
* condition. `@media print` is 1 (no decision), while
|
|
895
|
+
* `@supports (a:1) and (b:2) and (c:3)` is 3 and `not (a)` is 2.
|
|
896
|
+
*
|
|
897
|
+
* WHY THIS EXISTS. Complexity was measured for SELECTOR symbols only, and
|
|
898
|
+
* `pool-worker.ts` computes `complexity: s.complexity || s.metadata?.complexity
|
|
899
|
+
* || 1` — so every other kind's absent value became a FABRICATED 1 in the store.
|
|
900
|
+
* Measured on the corpus, that was 6,486 of 27,322 css symbols (24%): 4,664
|
|
901
|
+
* `variable`, 1,378 `interface` (@supports), 350 `module` (@media/@layer/
|
|
902
|
+
* @container) and 94 `function` (keyframes). A `@supports` scope with a
|
|
903
|
+
* 20-condition query read identically to `.plain { color: red }`, which is the
|
|
904
|
+
* same defect as the hardcoded 1, one level narrower and therefore easier to
|
|
905
|
+
* miss.
|
|
906
|
+
*/
|
|
907
|
+
queryComplexity(text) {
|
|
908
|
+
if (!text) return 1;
|
|
909
|
+
let depth = 0;
|
|
910
|
+
let ops = 0;
|
|
911
|
+
const src = text;
|
|
912
|
+
for (let i = 0; i < src.length; i++) {
|
|
913
|
+
const c = src[i];
|
|
914
|
+
if (c === "(") {
|
|
915
|
+
depth++;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (c === ")") {
|
|
919
|
+
depth--;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (depth !== 0) continue;
|
|
923
|
+
if (c === "a" && src.startsWith("and", i) && this.isSpace(src[i - 1])) {
|
|
924
|
+
ops++;
|
|
925
|
+
i += 2;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (c === "o" && src.startsWith("or", i) && this.isSpace(src[i - 1])) {
|
|
929
|
+
ops++;
|
|
930
|
+
i += 1;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (c === "n" && src.startsWith("not", i) && this.isSpace(src[i - 1])) {
|
|
934
|
+
ops++;
|
|
935
|
+
i += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return 1 + ops;
|
|
940
|
+
}
|
|
941
|
+
/** `keyframe_block` children of a `keyframes_statement` — one per animation stop. */
|
|
942
|
+
countKeyframeStops(node) {
|
|
943
|
+
let n = 0;
|
|
944
|
+
const walk = (x) => {
|
|
945
|
+
if (x.type === "keyframe_block") n++;
|
|
946
|
+
for (let i = 0; i < x.childCount; i++) {
|
|
947
|
+
const c = x.child(i);
|
|
948
|
+
if (c) walk(c);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
walk(node);
|
|
952
|
+
return n;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* A custom property's complexity: 1, plus one for each `var(--other)` it reads.
|
|
956
|
+
* A `--brand: #06c` is 1; a theming token composing five others is 6, and that
|
|
957
|
+
* composition chain is the review cost. This is the last css symbol kind that
|
|
958
|
+
* had no measured value — with it, all 27,322 corpus symbols carry one, and the
|
|
959
|
+
* `|| 1` fallback in `pool-worker.ts` stops fabricating a quarter of the lane.
|
|
960
|
+
*/
|
|
961
|
+
customPropertyComplexity(decl) {
|
|
962
|
+
let reads = 0;
|
|
963
|
+
for (let i = 0; i < decl.childCount; i++) {
|
|
964
|
+
const child = decl.child(i);
|
|
965
|
+
if (!child) continue;
|
|
966
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
967
|
+
const leaf = child.child(j);
|
|
968
|
+
if (leaf && this.countsVarRead(leaf.text)) reads++;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return 1 + reads;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Does this text contain a `var(` immediately followed by a custom-property name?
|
|
975
|
+
* Replaces `/var\(\s*--/` with a scan rather than narrowing it: the whitespace
|
|
976
|
+
* between `(` and `--` is skipped with the same `\s` set, so `var( --x)` still
|
|
977
|
+
* counts exactly as it did.
|
|
978
|
+
*/
|
|
979
|
+
countsVarRead(text) {
|
|
980
|
+
for (let i = 0; i + 3 < text.length; i++) {
|
|
981
|
+
if (text[i] !== "v" || text[i + 1] !== "a" || text[i + 2] !== "r" || text[i + 3] !== "(") continue;
|
|
982
|
+
let k = i + 4;
|
|
983
|
+
while (k < text.length && this.isSpace(text[k])) k++;
|
|
984
|
+
if (text[k] === "-" && text[k + 1] === "-") return true;
|
|
985
|
+
}
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* The at-rule's OWN descriptors: `declaration` children of its DIRECT block, one
|
|
990
|
+
* level only.
|
|
991
|
+
*
|
|
992
|
+
* One level, deliberately. The first version walked the whole subtree and
|
|
993
|
+
* produced complexity 2,917 for Tailwind's `@layer utilities`, which wraps
|
|
994
|
+
* essentially the whole stylesheet — so the number was "declarations in
|
|
995
|
+
* everything nested under here", which is a file size, not a complexity, and it
|
|
996
|
+
* double-counted every selector that already carries its own. `@layer utilities`
|
|
997
|
+
* is honestly 1: its contents are accounted by the rules inside it.
|
|
998
|
+
*
|
|
999
|
+
* This is what makes the descriptor reading correct where it applies:
|
|
1000
|
+
* `@property --x` with `syntax`/`inherits`/`initial-value` is 4, `@font-face`
|
|
1001
|
+
* with a descriptor set is its count + 1, and `@mixin` with one declaration is 2.
|
|
1002
|
+
*/
|
|
1003
|
+
countDescriptors(node) {
|
|
1004
|
+
let n = 0;
|
|
1005
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1006
|
+
const child = node.child(i);
|
|
1007
|
+
if (!child || child.type !== "block") continue;
|
|
1008
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
1009
|
+
const d = child.child(j);
|
|
1010
|
+
if (d && d.type === "declaration") n++;
|
|
1011
|
+
}
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
return n;
|
|
1015
|
+
}
|
|
1016
|
+
findChild(node, predicate) {
|
|
1017
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1018
|
+
const child = node.child(i);
|
|
1019
|
+
if (child && predicate(child)) return child;
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
filterChildren(node, predicate) {
|
|
1024
|
+
const res = [];
|
|
1025
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1026
|
+
const child = node.child(i);
|
|
1027
|
+
if (child && predicate(child)) res.push(child);
|
|
1028
|
+
}
|
|
1029
|
+
return res;
|
|
1030
|
+
}
|
|
1031
|
+
processRuleSet(node, media, supports, layer, container, scope, parentSelector, ctx) {
|
|
1032
|
+
const selectorsNode = this.findChild(node, (c) => c?.type === "selectors");
|
|
1033
|
+
if (!selectorsNode) return null;
|
|
1034
|
+
if (this.isRecoveryDebris(selectorsNode)) {
|
|
1035
|
+
ctx.errors.push({
|
|
1036
|
+
message: `Syntax ERROR: this selector is not a valid CSS rule (the grammar could not read the construct it belongs to), so nothing is extracted from it.`,
|
|
1037
|
+
range: this.nodeToRange(node),
|
|
1038
|
+
severity: `warning`
|
|
1039
|
+
});
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
if (ctx.fileHasParseError && this.isKeyframeStepSelector(selectorsNode)) {
|
|
1043
|
+
ctx.errors.push({
|
|
1044
|
+
message: `Syntax ERROR: a keyframe step was re-parsed as a rule by error recovery, so it is not extracted as a selector.`,
|
|
1045
|
+
range: this.nodeToRange(node),
|
|
1046
|
+
severity: `warning`
|
|
1047
|
+
});
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
if (!looksLikeSelectorText(selectorsNode.text)) return null;
|
|
1051
|
+
const rawParts = selectorsNode.text.split(",");
|
|
1052
|
+
const rawSelectors = [];
|
|
1053
|
+
for (let i = 0; i < rawParts.length; i++) {
|
|
1054
|
+
rawSelectors.push(rawParts[i].trim());
|
|
1055
|
+
}
|
|
1056
|
+
const keptSelectors = [];
|
|
1057
|
+
for (let i = 0; i < rawSelectors.length; i++) {
|
|
1058
|
+
if (!isResetSelector(rawSelectors[i])) keptSelectors.push(rawSelectors[i]);
|
|
1059
|
+
}
|
|
1060
|
+
if (keptSelectors.length === 0) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
if (keptSelectors.length !== rawSelectors.length) {
|
|
1064
|
+
rawSelectors.length = 0;
|
|
1065
|
+
for (let i = 0; i < keptSelectors.length; i++) rawSelectors.push(keptSelectors[i]);
|
|
1066
|
+
}
|
|
1067
|
+
let parentSelectors = [""];
|
|
1068
|
+
if (parentSelector) {
|
|
1069
|
+
const pParts = parentSelector.split(",");
|
|
1070
|
+
parentSelectors = [];
|
|
1071
|
+
for (let i = 0; i < pParts.length; i++) {
|
|
1072
|
+
parentSelectors.push(pParts[i].trim());
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
for (const r of rawSelectors) {
|
|
1076
|
+
if (!parentSelector && hasNestingAmp(r)) return null;
|
|
1077
|
+
}
|
|
1078
|
+
const resolvedSelectors = [];
|
|
1079
|
+
for (let i = 0; i < parentSelectors.length; i++) {
|
|
1080
|
+
const p = parentSelectors[i];
|
|
1081
|
+
for (let j = 0; j < rawSelectors.length; j++) {
|
|
1082
|
+
const r = rawSelectors[j];
|
|
1083
|
+
if (p) {
|
|
1084
|
+
if (hasNestingAmp(r)) {
|
|
1085
|
+
resolvedSelectors.push(replaceAll(r, "&", p));
|
|
1086
|
+
} else {
|
|
1087
|
+
resolvedSelectors.push(`${p} ${r}`);
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
resolvedSelectors.push(r);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const selectorText = resolvedSelectors.join(", ");
|
|
1095
|
+
let isComplex = false;
|
|
1096
|
+
let hasPseudoClass = false;
|
|
1097
|
+
let hasPseudoElement = false;
|
|
1098
|
+
let isUtility = true;
|
|
1099
|
+
let isCssModule = false;
|
|
1100
|
+
const classNames = [];
|
|
1101
|
+
let hasIdSelector = false;
|
|
1102
|
+
let hasTagSelector = false;
|
|
1103
|
+
let hasAttributeSelector = false;
|
|
1104
|
+
let maxSpecificity = [0, 0, 0];
|
|
1105
|
+
let groupComplexity = 0;
|
|
1106
|
+
const selectorNodes = selectorsNode.namedChildren;
|
|
1107
|
+
for (let j = 0; j < selectorNodes.length; j++) {
|
|
1108
|
+
const selNode = selectorNodes[j];
|
|
1109
|
+
let specId = 0;
|
|
1110
|
+
let specClass = 0;
|
|
1111
|
+
let specTag = 0;
|
|
1112
|
+
const chain = [];
|
|
1113
|
+
let selectorComplexity = 1;
|
|
1114
|
+
let nestingDepth = 0;
|
|
1115
|
+
const chainLines = {};
|
|
1116
|
+
const selStack = [selNode];
|
|
1117
|
+
while (selStack.length > 0) {
|
|
1118
|
+
const n = selStack.pop();
|
|
1119
|
+
if (n.type === "child_selector" || n.type === "sibling_selector" || n.type === "adjacent_sibling_selector" || n.type === "descendant_selector") {
|
|
1120
|
+
isComplex = true;
|
|
1121
|
+
isUtility = false;
|
|
1122
|
+
selectorComplexity += 1;
|
|
1123
|
+
}
|
|
1124
|
+
if (n.type === "pseudo_class_selector") {
|
|
1125
|
+
hasPseudoClass = true;
|
|
1126
|
+
specClass += 1;
|
|
1127
|
+
isUtility = false;
|
|
1128
|
+
selectorComplexity += 1;
|
|
1129
|
+
if (this.findChild(n, (c) => c?.type === "arguments") !== null) selectorComplexity += 1;
|
|
1130
|
+
}
|
|
1131
|
+
if (n.type === "pseudo_element_selector") {
|
|
1132
|
+
hasPseudoElement = true;
|
|
1133
|
+
specTag += 1;
|
|
1134
|
+
isUtility = false;
|
|
1135
|
+
selectorComplexity += 1;
|
|
1136
|
+
}
|
|
1137
|
+
if (n.type === "attribute_selector") {
|
|
1138
|
+
hasAttributeSelector = true;
|
|
1139
|
+
specClass += 1;
|
|
1140
|
+
isUtility = false;
|
|
1141
|
+
selectorComplexity += 1;
|
|
1142
|
+
const attrName = this.findChild(n, (c) => c?.type === "attribute_name");
|
|
1143
|
+
const attrKey = attrName ? attrName.text : n.text;
|
|
1144
|
+
chain.push(attrKey);
|
|
1145
|
+
chainLines[attrKey] = this.lineOf(n) ?? chainLines[attrKey];
|
|
1146
|
+
}
|
|
1147
|
+
if (n.type === "id_selector") {
|
|
1148
|
+
hasIdSelector = true;
|
|
1149
|
+
specId += 1;
|
|
1150
|
+
isUtility = false;
|
|
1151
|
+
chain.push(n.text);
|
|
1152
|
+
chainLines[n.text] = this.lineOf(n) ?? chainLines[n.text];
|
|
1153
|
+
}
|
|
1154
|
+
if (n.type === "tag_name") {
|
|
1155
|
+
hasTagSelector = true;
|
|
1156
|
+
specTag += 1;
|
|
1157
|
+
if (n.text !== "*") isUtility = false;
|
|
1158
|
+
chain.push(n.text);
|
|
1159
|
+
chainLines[n.text] = this.lineOf(n) ?? chainLines[n.text];
|
|
1160
|
+
}
|
|
1161
|
+
if (n.type === "class_name") {
|
|
1162
|
+
specClass += 1;
|
|
1163
|
+
classNames.push(n.text);
|
|
1164
|
+
chain.push(`.${n.text}`);
|
|
1165
|
+
chainLines[`.${n.text}`] = this.lineOf(n) ?? chainLines[`.${n.text}`];
|
|
1166
|
+
}
|
|
1167
|
+
if (n.type === "universal_selector") {
|
|
1168
|
+
specTag += 1;
|
|
1169
|
+
chain.push(n.text);
|
|
1170
|
+
chainLines[n.text] = this.lineOf(n) ?? chainLines[n.text];
|
|
1171
|
+
}
|
|
1172
|
+
if (n.text.indexOf(":local") !== -1 || n.text.indexOf(":global") !== -1) {
|
|
1173
|
+
isCssModule = true;
|
|
1174
|
+
}
|
|
1175
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1176
|
+
const child = n.namedChild(i);
|
|
1177
|
+
if (child) selStack.push(child);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (parentSelector) nestingDepth += 1;
|
|
1181
|
+
if (selectorComplexity > groupComplexity) groupComplexity = selectorComplexity;
|
|
1182
|
+
if (selectorComplexity > 0) ctx.fileSelectorComplexity = (ctx.fileSelectorComplexity ?? 0) + selectorComplexity;
|
|
1183
|
+
if (nestingDepth > 0) ctx.fileNestingDepth = Math.max(ctx.fileNestingDepth ?? 0, nestingDepth);
|
|
1184
|
+
if (selectorComplexity > nestingDepth) ctx.fileMaxSelectorComplexity = Math.max(ctx.fileMaxSelectorComplexity ?? 0, selectorComplexity);
|
|
1185
|
+
if (specId > maxSpecificity[0]) maxSpecificity = [specId, specClass, specTag];
|
|
1186
|
+
else if (specId === maxSpecificity[0] && specClass > maxSpecificity[1])
|
|
1187
|
+
maxSpecificity = [specId, specClass, specTag];
|
|
1188
|
+
else if (specId === maxSpecificity[0] && specClass === maxSpecificity[1] && specTag > maxSpecificity[2])
|
|
1189
|
+
maxSpecificity = [specId, specClass, specTag];
|
|
1190
|
+
if (selectorText) {
|
|
1191
|
+
const bar = selectorText.indexOf("|");
|
|
1192
|
+
if (bar > 0) {
|
|
1193
|
+
const prefix = selectorText.slice(0, bar).trim();
|
|
1194
|
+
const declared = ctx.namespacePrefixes[prefix];
|
|
1195
|
+
if (declared) {
|
|
1196
|
+
ctx.relationships.push({
|
|
1197
|
+
source: selectorText,
|
|
1198
|
+
target: declared,
|
|
1199
|
+
type: ASTEngine.RelationshipType.References,
|
|
1200
|
+
// the name on this edge is the DECLARATION, which is written on the
|
|
1201
|
+
// `@namespace` line — not the selector's own line
|
|
1202
|
+
line_number: this.lineOf(selectorsNode),
|
|
1203
|
+
metadata: { cssSpecific: true, isNamespaceRef: true }
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (isComplex && chain.length >= 2) {
|
|
1209
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
1210
|
+
ctx.deferredChainEdges.push({
|
|
1211
|
+
from: chain[i],
|
|
1212
|
+
to: chain[i + 1],
|
|
1213
|
+
rule: rawSelectors.join(", "),
|
|
1214
|
+
line: chainLines[chain[i + 1]] ?? this.lineOf(selectorsNode)
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
for (let i = 0; i < chain.length; i++) {
|
|
1218
|
+
if (chain[i].length === 0) continue;
|
|
1219
|
+
ctx.selectorChains[rawSelectors.join(", ")] = ctx.selectorChains[rawSelectors.join(", ")] ?? [];
|
|
1220
|
+
const list = ctx.selectorChains[rawSelectors.join(", ")];
|
|
1221
|
+
if (list.indexOf(chain[i]) === -1) list.push(chain[i]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (classNames.length !== 1 || isComplex || hasPseudoClass || hasPseudoElement || parentSelector) {
|
|
1226
|
+
isUtility = false;
|
|
1227
|
+
}
|
|
1228
|
+
const selectorKind = this.determineSelectorKind(
|
|
1229
|
+
hasIdSelector,
|
|
1230
|
+
classNames.length > 0,
|
|
1231
|
+
hasTagSelector,
|
|
1232
|
+
hasPseudoClass,
|
|
1233
|
+
hasPseudoElement,
|
|
1234
|
+
hasAttributeSelector,
|
|
1235
|
+
isComplex
|
|
1236
|
+
);
|
|
1237
|
+
if (parentSelector && selectorText === parentSelector) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
ctx.symbols.push({
|
|
1241
|
+
name: selectorText,
|
|
1242
|
+
kind: selectorKind,
|
|
1243
|
+
range: this.nodeToRange(node),
|
|
1244
|
+
scope: parentSelector,
|
|
1245
|
+
visibility: "public",
|
|
1246
|
+
// `complexity` is a REQUIRED field of the Symbol contract (engine.ts:389) and
|
|
1247
|
+
// the store reads it directly (`complexity: meta.complexity || 0`, engine.ts
|
|
1248
|
+
// :1573). CSS carried the number in `metadata` only, so the contract field was
|
|
1249
|
+
// never populated and both the store and pool-worker's `||` fallbacks invented
|
|
1250
|
+
// a value. One number, one field.
|
|
1251
|
+
complexity: groupComplexity + (parentSelector ? ctx.fileNestingDepth ?? 0 : 0),
|
|
1252
|
+
metadata: {
|
|
1253
|
+
isSelector: true,
|
|
1254
|
+
isUtility,
|
|
1255
|
+
isComplex,
|
|
1256
|
+
hasPseudoClass,
|
|
1257
|
+
hasPseudoElement,
|
|
1258
|
+
isCssModule,
|
|
1259
|
+
classNames,
|
|
1260
|
+
specificity: maxSpecificity,
|
|
1261
|
+
cssSpecific: true,
|
|
1262
|
+
// Nesting depth is added to the selector's own complexity (set on the
|
|
1263
|
+
// symbol above) rather than inside the walk, because `nestingDepth` is a
|
|
1264
|
+
// property of the RULE, and the walk counts complexity per selector within
|
|
1265
|
+
// the group.
|
|
1266
|
+
selectorComposition: {
|
|
1267
|
+
hasId: hasIdSelector,
|
|
1268
|
+
hasClass: classNames.length > 0,
|
|
1269
|
+
hasTag: hasTagSelector,
|
|
1270
|
+
hasPseudoClass,
|
|
1271
|
+
hasPseudoElement,
|
|
1272
|
+
hasAttribute: hasAttributeSelector
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
if (isComplex) ctx.patterns["Complex Selectors"] = true;
|
|
1277
|
+
if (isUtility) ctx.patterns["Utility-First Classes"] = true;
|
|
1278
|
+
if (hasPseudoClass) ctx.patterns["Pseudo-classes"] = true;
|
|
1279
|
+
if (hasPseudoElement) ctx.patterns["Pseudo-elements"] = true;
|
|
1280
|
+
if (isCssModule) {
|
|
1281
|
+
ctx.patterns["CSS Modules (:local/:global)"] = true;
|
|
1282
|
+
ctx.frameworks["CSS Modules"] = true;
|
|
1283
|
+
}
|
|
1284
|
+
const selectorLine = this.lineOf(selectorsNode);
|
|
1285
|
+
const asWritten = selectorsNode.text.trim();
|
|
1286
|
+
const composedName = collapseWhitespace(asWritten) !== selectorText;
|
|
1287
|
+
const nameLine = { line_number: selectorLine };
|
|
1288
|
+
const nameProvenance = composedName ? { isSynthesizedName: true, asWrittenSelector: asWritten } : {};
|
|
1289
|
+
if (media) {
|
|
1290
|
+
ctx.relationships.push({
|
|
1291
|
+
source: media,
|
|
1292
|
+
target: selectorText,
|
|
1293
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1294
|
+
...nameLine,
|
|
1295
|
+
metadata: { cssSpecific: true, isMediaScope: true, ...nameProvenance }
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (supports) {
|
|
1299
|
+
ctx.relationships.push({
|
|
1300
|
+
source: supports,
|
|
1301
|
+
target: selectorText,
|
|
1302
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1303
|
+
...nameLine,
|
|
1304
|
+
metadata: { cssSpecific: true, isSupportsScope: true, ...nameProvenance }
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
if (layer) {
|
|
1308
|
+
ctx.relationships.push({
|
|
1309
|
+
source: layer,
|
|
1310
|
+
target: selectorText,
|
|
1311
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1312
|
+
...nameLine,
|
|
1313
|
+
metadata: { cssSpecific: true, isLayerScope: true, ...nameProvenance }
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (container) {
|
|
1317
|
+
ctx.relationships.push({
|
|
1318
|
+
source: container,
|
|
1319
|
+
target: selectorText,
|
|
1320
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1321
|
+
...nameLine,
|
|
1322
|
+
metadata: { cssSpecific: true, isContainerScope: true, ...nameProvenance }
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (scope) {
|
|
1326
|
+
ctx.relationships.push({
|
|
1327
|
+
source: scope,
|
|
1328
|
+
target: selectorText,
|
|
1329
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1330
|
+
...nameLine,
|
|
1331
|
+
metadata: { cssSpecific: true, isScopeScope: true, ...nameProvenance }
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (parentSelector) {
|
|
1335
|
+
ctx.relationships.push({
|
|
1336
|
+
source: parentSelector,
|
|
1337
|
+
target: selectorText,
|
|
1338
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1339
|
+
...nameLine,
|
|
1340
|
+
metadata: {
|
|
1341
|
+
cssSpecific: true,
|
|
1342
|
+
isNestedSelector: true,
|
|
1343
|
+
...nameProvenance
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return selectorText;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* The node that NAMES the module an `@import` pulls in, or `null` when the statement
|
|
1351
|
+
* carries none.
|
|
1352
|
+
*
|
|
1353
|
+
* A DIRECT `string_value` / `call_expression` child is the shape the grammar produces for
|
|
1354
|
+
* a well-formed `@import`, and it stays the first thing tried so the 243 edges that were
|
|
1355
|
+
* already published are untouched.
|
|
1356
|
+
*
|
|
1357
|
+
* The DESCENDANT pass exists because pass 10 measured a 7-edge gap on the admitted corpus:
|
|
1358
|
+
* 250 `import_statement` nodes, 243 published `imports` edges
|
|
1359
|
+
* (`validation/m14/census/css-import-coverage.ts`). The 7 are indented-Sass `.sass`
|
|
1360
|
+
* partials, which this pinned CSS grammar cannot parse, and it recovers them into an
|
|
1361
|
+
* ERROR shape — `;+@import+ERROR+binary_expression`, `;+@import+ERROR+ERROR+float_value+
|
|
1362
|
+
* keyword_query` and four siblings — where the quoted specifier is still a real
|
|
1363
|
+
* `string_value` node but sits under the ERROR rather than directly under the statement.
|
|
1364
|
+
* `@import "../utilities/mixins"` is a genuine fetch the user agent performs; publishing
|
|
1365
|
+
* nothing for it is a silent miss, not a ruling.
|
|
1366
|
+
*
|
|
1367
|
+
* **The line guard is what makes the descendant pass safe, and it is not optional.** The
|
|
1368
|
+
* ERROR recovery SWALLOWS the rest of the file into the `import_statement` node:
|
|
1369
|
+
* `Documenter.jl/.../bulma/elements/button.sass` spans L2..L50 and carries ELEVEN
|
|
1370
|
+
* `string_value` descendants — one at L2 (`"../utilities/mixins"`, the module) and ten at
|
|
1371
|
+
* L47 (the `$breakpoints` map's `"mobile"`, `"small"`, `"normal"`, … ). Taking the first
|
|
1372
|
+
* descendant in document order happens to pick the right one there, and would pick the
|
|
1373
|
+
* WRONG one for any statement whose own specifier is not a node. Restricting the search to
|
|
1374
|
+
* the `@import` KEYWORD's own row rejects the L47 ten by construction, so the name is
|
|
1375
|
+
* always the specifier written on the `@import` line and never a string the recovery
|
|
1376
|
+
* swept up from a later line.
|
|
1377
|
+
*
|
|
1378
|
+
* Node-type and position tests only. No pattern over raw text, so a quoted word in a
|
|
1379
|
+
* comment, in a `content:` value or in a class name cannot become a module name.
|
|
1380
|
+
*/
|
|
1381
|
+
findImportNameNode(node) {
|
|
1382
|
+
const isNameNode = (n) => n.type === "string_value" || n.type === "call_expression";
|
|
1383
|
+
const direct = this.findChild(node, isNameNode);
|
|
1384
|
+
if (direct) return direct;
|
|
1385
|
+
const keyword = this.findChild(node, (c) => c?.type === "@import");
|
|
1386
|
+
const keywordRow = (keyword ?? node).startPosition.row;
|
|
1387
|
+
let found = null;
|
|
1388
|
+
const walk = (n) => {
|
|
1389
|
+
if (found) return;
|
|
1390
|
+
if (n.startPosition.row === keywordRow && isNameNode(n)) {
|
|
1391
|
+
found = n;
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1395
|
+
const child = n.child(i);
|
|
1396
|
+
if (child) walk(child);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
walk(node);
|
|
1400
|
+
return found;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The name an `imports` TARGET is published under, which is the same rule the SOURCE half
|
|
1404
|
+
* already uses: a stylesheet is named by its LAST PATH SEGMENT.
|
|
1405
|
+
*
|
|
1406
|
+
* ## The defect this fixes
|
|
1407
|
+
*
|
|
1408
|
+
* One edge had its two ends spelled by two different rules. The source is
|
|
1409
|
+
* `this.importerName` = `lastPathSegment(filePath)` = `layout.css`. The target was the
|
|
1410
|
+
* specifier verbatim = `styles/layout.css`. So the store could never join them, and
|
|
1411
|
+
* MEASURED over the 600-file window (`css-import-specifier-names.json`, 58 import edges):
|
|
1412
|
+
* 11 edges named a file that exists under the basename with that basename UNIQUE in its own
|
|
1413
|
+
* repo, and all 11 were unreachable.
|
|
1414
|
+
*
|
|
1415
|
+
* ## Why only stylesheet-extension specifiers
|
|
1416
|
+
*
|
|
1417
|
+
* A specifier's last segment is only a stylesheet's name when it is a stylesheet. Three
|
|
1418
|
+
* cases in the same 58-edge population are NOT, and reducing them would FABRICATE a
|
|
1419
|
+
* resolution:
|
|
1420
|
+
*
|
|
1421
|
+
* `https://fonts.googleapis.com/css?family=Lato:400,400i` (10 edges) — the last segment
|
|
1422
|
+
* names no file anywhere; it would bind to whatever unrelated file shares its basename.
|
|
1423
|
+
* `tailwindcss`, `tw-animate-css` (24 edges) — package names, resolved by a package
|
|
1424
|
+
* manager or a PostCSS plugin. `tailwindcss` DOES collide with a real file in this
|
|
1425
|
+
* corpus, so recovering it resolves to the wrong thing.
|
|
1426
|
+
* `@fontsource-variable/figtree/index.css` (1 edge) — a package SUBPATH. The `index.css`
|
|
1427
|
+
* last segment is the package's internal file, not this repo's.
|
|
1428
|
+
*
|
|
1429
|
+
* The discriminator is a final segment carrying a stylesheet extension, checked by
|
|
1430
|
+
* `charCodeAt` and `startsWith` — no pattern over text. `_variables.less` (9 edges, no
|
|
1431
|
+
* slash) is the case that proves the discriminator has to be the EXTENSION and not the
|
|
1432
|
+
* slash: it is a sibling file, and a slash test files it as a package.
|
|
1433
|
+
*
|
|
1434
|
+
* ## What this does NOT claim
|
|
1435
|
+
*
|
|
1436
|
+
* A basename held by more than one stylesheet in the same repo USED to be published under the
|
|
1437
|
+
* bare basename and stay ambiguous, on the ground that any disambiguator would be an invented
|
|
1438
|
+
* naming convention. That is now falsified and fixed: a relative specifier whose sibling exists
|
|
1439
|
+
* on disk targets the SIBLING's own published module name (`siblingImportTarget` above), which
|
|
1440
|
+
* is the file's existing identity rather than a new convention. Measured over the whole
|
|
1441
|
+
* `css,less` population: 51 ambiguous edges resolved, 0 currently-resolving edges put at risk.
|
|
1442
|
+
*/
|
|
1443
|
+
/**
|
|
1444
|
+
* CSS-D18-FALSIFIER. A RELATIVE specifier names the file BESIDE the importing stylesheet, and
|
|
1445
|
+
* when that file exists on disk the target is that sibling's OWN project-unique module name —
|
|
1446
|
+
* `<its file scope>::<its basename>` — which is not an invented convention but the identity
|
|
1447
|
+
* `publishFileScopedIdentities` already publishes for every file in this lane.
|
|
1448
|
+
*
|
|
1449
|
+
* WHY THIS REPLACES THE LANE'S EARLIER RULING. The paragraph below used to say that a basename
|
|
1450
|
+
* held by more than one stylesheet in a repo "stays AMBIGUOUS, because ... a disambiguator
|
|
1451
|
+
* invented here would be a second, invented naming convention". The falsifier was measured
|
|
1452
|
+
* (`validation/m14/fixtures/css/imports-sibling-falsifier.ts`, whole `css,less` population):
|
|
1453
|
+
* of 106 basename-specifier imports, the 51 whose basename TWO OR MORE admitted files publish
|
|
1454
|
+
* have an ADMITTED SIBLING in **51 of 51** cases, and of the 55 unique-basename ones the 32
|
|
1455
|
+
* with no admitted sibling **do not exist on disk at all** (dangling imports). So an
|
|
1456
|
+
* existence-gated resolution fixes every ambiguous edge and puts NONE of the currently
|
|
1457
|
+
* resolving ones at risk — the risk is measured at ZERO, not assumed.
|
|
1458
|
+
*
|
|
1459
|
+
* `existsSync` is a filesystem read in an analyzer, and that is stated rather than hidden: it
|
|
1460
|
+
* makes the target depend on which sibling is on disk, and a sibling that exists but is NOT
|
|
1461
|
+
* admitted would name a declaration the population does not contain (an unresolved edge where
|
|
1462
|
+
* the bare basename used to resolve). MEASURED on this population: 0 such cases in either
|
|
1463
|
+
* bucket. The alternative — naming a same-basename file in another directory — is what the
|
|
1464
|
+
* bare spelling already does, and it is right only by luck.
|
|
1465
|
+
*/
|
|
1466
|
+
siblingImportFile(specifier) {
|
|
1467
|
+
if (this.fileDir === "" || specifier.length === 0) return null;
|
|
1468
|
+
if (specifier.startsWith("/")) return null;
|
|
1469
|
+
const sibling = path.resolve(this.fileDir, specifier);
|
|
1470
|
+
if (!existsSync(sibling)) return null;
|
|
1471
|
+
return sibling;
|
|
1472
|
+
}
|
|
1473
|
+
siblingImportTarget(specifier) {
|
|
1474
|
+
const sibling = this.siblingImportFile(specifier);
|
|
1475
|
+
if (sibling === null) return null;
|
|
1476
|
+
const base = lastPathSegment(sibling);
|
|
1477
|
+
if (base.length === 0) return null;
|
|
1478
|
+
const scope = cssFileScopeOf(sibling);
|
|
1479
|
+
if (scope === "") return null;
|
|
1480
|
+
return `${scope}::${base}`;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* THE CLAIM AN `@import` TARGET CAN CARRY — the reason the edge names something this run cannot
|
|
1484
|
+
* resolve, published on a key the ARBITER reads rather than in a comment.
|
|
1485
|
+
*
|
|
1486
|
+
* THE DEFECT THIS CLOSES, in this lane's own measurement. The gate's residual for `css` over the
|
|
1487
|
+
* 400-file stride is **85 edges = 70 ambiguous + 15 unresolved**; the 15 are ALL `@import`
|
|
1488
|
+
* specifiers that name no declaration in the sampled key space, and they carried NO claim of any
|
|
1489
|
+
* kind — the marker table showed `imports importSource+importSpecifier+isCssImport` with
|
|
1490
|
+
* `extClaim 0`. This is the fleet-wide finding (`RESIDUAL-CLASSIFICATION-41-LANES.md:18-34`) in
|
|
1491
|
+
* its earlier form: not a truthful marker on an unread spelling, but no marker at all, on a
|
|
1492
|
+
* construct whose reason the analyzer ALREADY computes — `importTargetName` above branches on
|
|
1493
|
+
* network URL vs project path vs package name.
|
|
1494
|
+
*
|
|
1495
|
+
* THE TWO SHAPES THE CENSUS FOUND, and each one's claim:
|
|
1496
|
+
*
|
|
1497
|
+
* A NETWORK URL (`https://fonts.googleapis.com/css?family=…`, 7 of the 15) is outside every
|
|
1498
|
+
* admitted file list BY CONSTRUCTION, so the edge carries `sourcePathClaim`. Its polarity is
|
|
1499
|
+
* inverted on purpose (`incoming-rule.ts`, `SOURCE_PATH_CLAIM_KEY`): the project REFUTES the
|
|
1500
|
+
* claim the moment a candidate IS an admitted file, which is what stops this from becoming a
|
|
1501
|
+
* laundering channel. MEASURED, with the arbiter's own `classifyEdge` and its own admitted
|
|
1502
|
+
* file facts: **7 of 7 verify, 0 refuted** — and the spelling cannot produce a refutation,
|
|
1503
|
+
* since no admitted path answers a URL.
|
|
1504
|
+
*
|
|
1505
|
+
* A RELATIVE SPECIFIER THAT RESOLVES TO A FILE BESIDE THIS ONE (`styles/layout.css`,
|
|
1506
|
+
* `MavenPro.css`; 8 of the 15) is a dependency the project plainly HAS — it is on disk, and it
|
|
1507
|
+
* is an admitted file — but the sampler did not draw the file that declares it, so the target
|
|
1508
|
+
* (`<scope>::<basename>`) answers nothing in the key space. The honest claim is
|
|
1509
|
+
* `importProjectFile`: the project's own admitted list CONFIRMS the file, which is evidence the
|
|
1510
|
+
* analyzer never saw. MEASURED the same way: **8 of 8 verify, 0 refuted** — the candidate is
|
|
1511
|
+
* the sibling's real repo-relative path (`plugins/org.jkiss.dbeaver.ui.ai/web/styles/layout.css`,
|
|
1512
|
+
* …) and every one of the eight is in the admitted file list. This is the yaml `!include`
|
|
1513
|
+
* claim's law (`incoming-rule.ts`, `FILE_CLAIM_KEY`): a miss would REFUTE, so existence is
|
|
1514
|
+
* checked before the claim is published — never the other way round.
|
|
1515
|
+
*
|
|
1516
|
+
* WHAT IS DELIBERATELY LEFT UNMARKED, which is the half that keeps this from being a blanket
|
|
1517
|
+
* excuse. A BARE PACKAGE NAME (`tailwindcss`, `tw-animate-css`) gets NOTHING: the source-path
|
|
1518
|
+
* claim is defined only for a target containing `/` or a scheme, and a bare name is exactly the
|
|
1519
|
+
* spelling that can also name a DECLARATION. An `@`-SCOPED PACKAGE SUBPATH
|
|
1520
|
+
* (`@fontsource-variable/figtree/index.css`) gets nothing either: it is not in this population,
|
|
1521
|
+
* and its claim's CANDIDATES include the bare basename (`index.css`), which a repo that admits a
|
|
1522
|
+
* root-level `index.css` would refute. A claim that has not been measured is not published.
|
|
1523
|
+
*
|
|
1524
|
+
* EVERY KEY HERE IS ADDITIVE. The target is untouched — no rebasing, no basename stripping — so
|
|
1525
|
+
* `resolved`, `collRes` and the binding-intent table cannot move; the claim is read by the
|
|
1526
|
+
* arbiter only where the key space declares nothing by that target (`classifyEdge`'s
|
|
1527
|
+
* `decls === 0` branch), so no existing resolution can be relabelled either.
|
|
1528
|
+
*/
|
|
1529
|
+
importClaimMetadata(specifier) {
|
|
1530
|
+
if (this.claimProjectRoot.length === 0) return {};
|
|
1531
|
+
if (specifier.startsWith("http:") || specifier.startsWith("https:") || specifier.startsWith("//")) {
|
|
1532
|
+
return { sourcePathClaim: [specifier] };
|
|
1533
|
+
}
|
|
1534
|
+
if (specifier.startsWith("@") || specifier.startsWith("~")) return {};
|
|
1535
|
+
const sibling = this.siblingImportFile(specifier);
|
|
1536
|
+
if (sibling === null) return {};
|
|
1537
|
+
try {
|
|
1538
|
+
if (!statSync(sibling).isFile()) return {};
|
|
1539
|
+
} catch {
|
|
1540
|
+
return {};
|
|
1541
|
+
}
|
|
1542
|
+
const rel = path.relative(this.claimProjectRoot, sibling);
|
|
1543
|
+
if (rel.length === 0 || rel.startsWith("..")) return {};
|
|
1544
|
+
return { importProjectFile: [rel.split(path.sep).join("/")] };
|
|
1545
|
+
}
|
|
1546
|
+
importTargetName(specifier) {
|
|
1547
|
+
if (specifier.startsWith("http:") || specifier.startsWith("https:") || specifier.startsWith("//")) return specifier;
|
|
1548
|
+
if (specifier.startsWith("@") || specifier.startsWith("~")) return specifier;
|
|
1549
|
+
const sibling = this.siblingImportTarget(specifier);
|
|
1550
|
+
if (sibling !== null) return sibling;
|
|
1551
|
+
const slash = specifier.lastIndexOf("/");
|
|
1552
|
+
if (slash < 0) return specifier;
|
|
1553
|
+
const segment = specifier.slice(slash + 1);
|
|
1554
|
+
const dot = segment.lastIndexOf(".");
|
|
1555
|
+
if (dot <= 0) return specifier;
|
|
1556
|
+
const ext = segment.slice(dot).toLowerCase();
|
|
1557
|
+
for (const known of [".css", ".scss", ".sass", ".less"]) {
|
|
1558
|
+
if (ext === known) return segment;
|
|
1559
|
+
}
|
|
1560
|
+
return specifier;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Is this `@import` the PLAIN FILE FORM — the one that names a stylesheet to fold in?
|
|
1564
|
+
*
|
|
1565
|
+
* Sass and Less inline a non-CSS import at compile time, so that form is an inclusion of one
|
|
1566
|
+
* source file into another, not a stylesheet load. The grammar answers the question as a SHAPE,
|
|
1567
|
+
* which is why this reads the tree rather than matching spellings:
|
|
1568
|
+
* `@import url("x")` → one named child, type `call_expression` (stylesheet load)
|
|
1569
|
+
* `@import "x" screen` → a second named child (the media query) (stylesheet load)
|
|
1570
|
+
* `@import "x.css";` → one `string_value` whose text is a CSS file (stylesheet load)
|
|
1571
|
+
* so the plain-file form is: exactly ONE named child, type `string_value`, and a specifier that
|
|
1572
|
+
* is not a `.css` file, not a protocol URL, not a `sass:` built-in module and not `data:`.
|
|
1573
|
+
* Error-recovered statements fail the shape test and stay `imports`.
|
|
1574
|
+
*/
|
|
1575
|
+
isPlainFileFormImport(node) {
|
|
1576
|
+
const named = [];
|
|
1577
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1578
|
+
const c = node.namedChild(i);
|
|
1579
|
+
if (c) named.push(c);
|
|
1580
|
+
}
|
|
1581
|
+
if (named.length !== 1 || named[0].type !== "string_value") return false;
|
|
1582
|
+
const spec = stripAllQuotes(named[0].text).trim();
|
|
1583
|
+
if (spec.length === 0) return false;
|
|
1584
|
+
if (hasCssFileExtension(spec)) return false;
|
|
1585
|
+
const lower = spec.toLowerCase();
|
|
1586
|
+
if (lower.startsWith("//") || lower.startsWith("http://") || lower.startsWith("https://")) return false;
|
|
1587
|
+
if (lower.startsWith("sass:") || lower.startsWith("data:")) return false;
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
processImport(node, ctx) {
|
|
1591
|
+
const stringNode = this.findImportNameNode(node);
|
|
1592
|
+
if (stringNode) {
|
|
1593
|
+
const moduleName = unwrapUrl(stringNode.text);
|
|
1594
|
+
const targetName = this.importTargetName(moduleName);
|
|
1595
|
+
ctx.imports.push({
|
|
1596
|
+
module: moduleName,
|
|
1597
|
+
source: moduleName,
|
|
1598
|
+
imported: ["*"],
|
|
1599
|
+
isTypeOnly: false,
|
|
1600
|
+
range: this.nodeToRange(node)
|
|
1601
|
+
});
|
|
1602
|
+
const isPart = this.cssFamilyHost && this.isPlainFileFormImport(node);
|
|
1603
|
+
ctx.relationships.push({
|
|
1604
|
+
source: this.importerName,
|
|
1605
|
+
target: targetName,
|
|
1606
|
+
type: isPart ? ASTEngine.RelationshipType.PartOf : ASTEngine.RelationshipType.Imports,
|
|
1607
|
+
// the name on this edge is the MODULE, so the line is the
|
|
1608
|
+
// `string_value` / `call_expression` node — which can be on a LATER line
|
|
1609
|
+
// than the `@import` keyword, and is for the multi-line form where the
|
|
1610
|
+
// url() wraps its argument. Taking the statement's line would be the
|
|
1611
|
+
// enclosing construct, not the name.
|
|
1612
|
+
line_number: this.lineOf(stringNode),
|
|
1613
|
+
confidence: 1,
|
|
1614
|
+
metadata: {
|
|
1615
|
+
isCssImport: true,
|
|
1616
|
+
importSource: this.importerName,
|
|
1617
|
+
importSpecifier: moduleName,
|
|
1618
|
+
...isPart ? { partRole: "library", isCssPartImport: true } : {},
|
|
1619
|
+
// THE REASON, on the channel the arbiter reads. `imports` was the only residual type of
|
|
1620
|
+
// this lane that published NO claim at all (gate marker table: `extClaim 0`), while the
|
|
1621
|
+
// analyzer already knew which specifiers are network URLs and which name a project file.
|
|
1622
|
+
// Additive: the target is untouched, so no resolution can move — see
|
|
1623
|
+
// `importClaimMetadata` for the measured 15 of 15 verifications and what is NOT marked.
|
|
1624
|
+
...this.importClaimMetadata(moduleName)
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
if (moduleName.indexOf("tailwind") !== -1) ctx.frameworks["Tailwind CSS"] = true;
|
|
1628
|
+
if (moduleName.indexOf("bootstrap") !== -1) ctx.frameworks["Bootstrap"] = true;
|
|
1629
|
+
if (moduleName.indexOf("bulma") !== -1) ctx.frameworks["Bulma"] = true;
|
|
1630
|
+
if (moduleName.indexOf("foundation") !== -1) ctx.frameworks["Foundation"] = true;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Every custom-media name a media query REFERENCES, in source order.
|
|
1635
|
+
*
|
|
1636
|
+
* Custom Media Queries L1: `@media (--narrow)` evaluates a `@custom-media`
|
|
1637
|
+
* declaration, wherever in the query list it appears and however the query is
|
|
1638
|
+
* composed. The grammar marks the reference exactly — a `keyword_query` directly
|
|
1639
|
+
* under a `parenthesized_query`, spelled `--name` — so this reads that shape rather
|
|
1640
|
+
* than recognising a spelling, which is why `@media screen and (--narrow)`,
|
|
1641
|
+
* `@media not (--c)` and `@media (--a), (--b)` are all covered.
|
|
1642
|
+
*
|
|
1643
|
+
* The discriminator is the `--` prefix and NOT the node type alone, because
|
|
1644
|
+
* `keyword_query` is the grammar's node for a bare media type too: `screen` in
|
|
1645
|
+
* `@media screen` and `print` in `@media print and (color)` are the same node type
|
|
1646
|
+
* and neither is a custom-media reference.
|
|
1647
|
+
*
|
|
1648
|
+
* A `parenthesized_query` holding a `feature_query` — `(min-width: 400px)` — has no
|
|
1649
|
+
* `keyword_query` child at all, so it contributes nothing, which is the correct
|
|
1650
|
+
* answer without a special case.
|
|
1651
|
+
*/
|
|
1652
|
+
customMediaRefsIn(queryNode) {
|
|
1653
|
+
const out = [];
|
|
1654
|
+
const stack = [queryNode];
|
|
1655
|
+
while (stack.length > 0) {
|
|
1656
|
+
const n = stack.pop();
|
|
1657
|
+
const isRefSite = n.type === "parenthesized_query" || n === queryNode;
|
|
1658
|
+
if (isRefSite) {
|
|
1659
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1660
|
+
const c = n.namedChild(i);
|
|
1661
|
+
if (!c || c.type !== "keyword_query") continue;
|
|
1662
|
+
const name = leadingCustomIdent(c.text);
|
|
1663
|
+
if (name.length > 0 && out.indexOf(name) === -1) out.push(name);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1667
|
+
const c = n.namedChild(i);
|
|
1668
|
+
if (c) stack.push(c);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return out;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The `@custom-media` references of a whole comma-separated query LIST.
|
|
1675
|
+
*
|
|
1676
|
+
* `customMediaRefsIn` walks ONE node's subtree. A comma list is not inside one
|
|
1677
|
+
* node — the grammar leaves the terms as SIBLING query children of the statement —
|
|
1678
|
+
* so the single-term walk cannot see past the first comma. This unions the walk
|
|
1679
|
+
* over the statement's own DIRECT query children, which is what makes
|
|
1680
|
+
* `@media (--a), (--b)` depend on both `--a` and `--b`.
|
|
1681
|
+
*
|
|
1682
|
+
* Direct children only, so the walk cannot descend into the block and read a
|
|
1683
|
+
* declaration value as a reference. For a single-term query there is exactly one
|
|
1684
|
+
* such child, so the result is identical to `customMediaRefsIn(queryNode)` — which
|
|
1685
|
+
* is why this is a widening and not a rewrite.
|
|
1686
|
+
*/
|
|
1687
|
+
customMediaRefsInList(statement) {
|
|
1688
|
+
const out = [];
|
|
1689
|
+
for (let i = 0; i < statement.childCount; i++) {
|
|
1690
|
+
const c = statement.child(i);
|
|
1691
|
+
if (!c) continue;
|
|
1692
|
+
if (c.type !== "keyword_query" && c.type !== "binary_query" && c.type !== "feature_query" && c.type !== "parenthesized_query" && c.type !== "unary_query" && c.type !== "selector_query") {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
for (const name of this.customMediaRefsIn(c)) if (out.indexOf(name) === -1) out.push(name);
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
processMedia(node, ctx) {
|
|
1700
|
+
const queryNode = this.findChild(
|
|
1701
|
+
node,
|
|
1702
|
+
(c) => c && (c.type === "keyword_query" || c.type === "binary_query" || c.type === "feature_query" || c.type === "parenthesized_query" || c.type === "unary_query" || c.type === "selector_query")
|
|
1703
|
+
);
|
|
1704
|
+
if (queryNode) {
|
|
1705
|
+
const mediaName = `@media ${collapseWhitespace(this.atRulePrelude(node, "@media"))}`;
|
|
1706
|
+
ctx.patterns["Responsive Design (Media Queries)"] = true;
|
|
1707
|
+
if (!ctx.mediaScopes[mediaName]) {
|
|
1708
|
+
ctx.mediaScopes[mediaName] = true;
|
|
1709
|
+
const published = {
|
|
1710
|
+
name: mediaName,
|
|
1711
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
1712
|
+
range: this.nodeToRange(node),
|
|
1713
|
+
scope: "",
|
|
1714
|
+
visibility: "public",
|
|
1715
|
+
complexity: this.queryComplexity(collapseWhitespace(queryNode.text)),
|
|
1716
|
+
metadata: {
|
|
1717
|
+
isMediaScope: true,
|
|
1718
|
+
mediaCondition: collapseWhitespace(queryNode.text),
|
|
1719
|
+
scopeBlocks: 1
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
ctx.symbols.push(published);
|
|
1723
|
+
ctx.scopeSymbol[mediaName] = published;
|
|
1724
|
+
for (const declared of this.customMediaRefsInList(node)) {
|
|
1725
|
+
if (!ctx.customMediaNames[declared]) continue;
|
|
1726
|
+
ctx.relationships.push({
|
|
1727
|
+
source: mediaName,
|
|
1728
|
+
target: declared,
|
|
1729
|
+
type: ASTEngine.RelationshipType.References,
|
|
1730
|
+
// the NAME is the condition's own line, the same rule as the
|
|
1731
|
+
// font-family edge: the reference is the condition, not the statement
|
|
1732
|
+
line_number: this.lineOf(queryNode),
|
|
1733
|
+
metadata: { cssSpecific: true, isCustomMediaRef: true }
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
this.widenScopeSymbol(ctx.scopeSymbol[mediaName], node);
|
|
1738
|
+
}
|
|
1739
|
+
return mediaName;
|
|
1740
|
+
}
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Extend an already-published at-rule SCOPE symbol so its recorded extent covers a
|
|
1745
|
+
* further block of the same scope.
|
|
1746
|
+
*
|
|
1747
|
+
* ## What was wrong, and how it was found
|
|
1748
|
+
*
|
|
1749
|
+
* `@media` and `@layer` publish ONE symbol per name, at the FIRST block, and the
|
|
1750
|
+
* identity merge at the end of `analyze` never sees a second copy to fold. So a file
|
|
1751
|
+
* with `@media (min-width: 576px)` three times has one symbol for it, recorded at the
|
|
1752
|
+
* first block, and every rule of the second and third blocks receives a `contains`
|
|
1753
|
+
* edge pointing at that first block. The edge is TRUE — the rule is inside a block of
|
|
1754
|
+
* that query — and its endpoint is somewhere the rule is not.
|
|
1755
|
+
*
|
|
1756
|
+
* Measured over the whole admitted corpus: 7,257 of 25,181 `isMediaScope` edges
|
|
1757
|
+
* (28.8%) and 40 of 505 `isLayerScope` edges (7.9%) carried a contained rule outside
|
|
1758
|
+
* the source symbol's line range. Worked example, `bootstrap-grid.css`: the edge for
|
|
1759
|
+
* `.col-sm` is at L284 and the symbol is recorded at L22-L26.
|
|
1760
|
+
*
|
|
1761
|
+
* A pass-1 probe reported this as 0 violations. That number was VACUOUS — the probe
|
|
1762
|
+
* read the extent as `symbol.range.start.line`, and `parseFile` returns no `range`
|
|
1763
|
+
* object at all (it flattens to top-level `startLine` / `endLine`), so every lookup
|
|
1764
|
+
* missed. The non-zero figure is from the corrected probe, and the correction is
|
|
1765
|
+
* recorded in `validation/m14/census/css-boundary-probe.ts` because a broken probe
|
|
1766
|
+
* reporting zero is indistinguishable from a correct probe reporting zero.
|
|
1767
|
+
*
|
|
1768
|
+
* ## Why widening is the right repair and not publishing a second symbol
|
|
1769
|
+
*
|
|
1770
|
+
* Two symbols with one name is the identity collision this dedup exists to prevent —
|
|
1771
|
+
* an edge naming one of them is ambiguous, and an ambiguous endpoint is the failure
|
|
1772
|
+
* the export invariant convicts. And the NAME is the only identity a consumer has
|
|
1773
|
+
* here: `pool-worker.ts` forwards a narrow allowlist of `metadata` keys and
|
|
1774
|
+
* `isMediaScope` / `mediaCondition` are not on it, so 2,804 of 3,084 at-rule scope
|
|
1775
|
+
* symbols arrive from `parseFile` with NO metadata at all (measured, same probe). A
|
|
1776
|
+
* consumer cannot tell two `@media` nodes apart except by the name, so the name's
|
|
1777
|
+
* extent has to be the union of its blocks. `@supports` and `@container` already get
|
|
1778
|
+
* exactly this from the identity merge.
|
|
1779
|
+
*
|
|
1780
|
+
* ## Only `endLine` moves, and the gap it leaves is stated
|
|
1781
|
+
*
|
|
1782
|
+
* The identity merge widens `endLine` only, and this does the same, so the byte span
|
|
1783
|
+
* (`startIndex`/`endIndex`) still describes the FIRST block while the line extent
|
|
1784
|
+
* describes all of them. A range is therefore not contiguous for a scope with
|
|
1785
|
+
* several blocks. That is the lane's existing convention rather than a new one, and
|
|
1786
|
+
* it is recorded here because a consumer that assumes contiguity will be wrong for
|
|
1787
|
+
* exactly the multi-block scopes this method exists to describe. `scopeBlocks` counts
|
|
1788
|
+
* the blocks — and is itself dropped at the production boundary, for the reason above,
|
|
1789
|
+
* so it is available to a fixture and not to a store consumer.
|
|
1790
|
+
*/
|
|
1791
|
+
widenScopeSymbol(sym, block) {
|
|
1792
|
+
if (!sym || !sym.range) return;
|
|
1793
|
+
const r = this.nodeToRange(block);
|
|
1794
|
+
if (r.endLine <= (sym.range.endLine ?? 0)) return;
|
|
1795
|
+
sym.range = { ...sym.range, endLine: r.endLine };
|
|
1796
|
+
const md = sym.metadata ?? {};
|
|
1797
|
+
const n = typeof md.scopeBlocks === "number" ? md.scopeBlocks : 1;
|
|
1798
|
+
sym.metadata = { ...md, scopeBlocks: n + 1 };
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Every `@keyframes` name in the file, collected before the main walk so that
|
|
1802
|
+
* an `animation:` reference resolves regardless of where the block sits.
|
|
1803
|
+
*/
|
|
1804
|
+
/**
|
|
1805
|
+
* The published name of an `@font-face` declaration. Used by BOTH the symbol push
|
|
1806
|
+
* and the `uses` edge, because when they were written separately the edge named
|
|
1807
|
+
* the bare family while the symbol carried the keyword — a dangling endpoint, the
|
|
1808
|
+
* same defect fixed for `@property` one commit earlier, in the other direction.
|
|
1809
|
+
* One expression is the only way two names cannot drift apart.
|
|
1810
|
+
*/
|
|
1811
|
+
/**
|
|
1812
|
+
* The leading `--ident` of an at-rule prelude, or `""`.
|
|
1813
|
+
*
|
|
1814
|
+
* Written against the DECLARED name rather than a node type on purpose: the
|
|
1815
|
+
* grammar has no `custom_media` node at all (all three of these at-rules are a
|
|
1816
|
+
* bare `at_rule`), so there is nothing type-shaped to key on, and CSS itself says
|
|
1817
|
+
* a custom media query name is a `<custom-ident>` followed by a condition.
|
|
1818
|
+
*/
|
|
1819
|
+
atRulePrelude(node, keyword) {
|
|
1820
|
+
const kwNode = this.findChild(node, (c) => c?.type === "at_keyword") ?? // `@namespace` and `@media` carry their keyword as an ANONYMOUS child whose type
|
|
1821
|
+
// IS the keyword text, so the named-node lookup misses them.
|
|
1822
|
+
this.findChild(node, (c) => c?.type === keyword);
|
|
1823
|
+
const block = this.findChild(node, (c) => c?.type === "block");
|
|
1824
|
+
const semi = this.findChild(node, (c) => c?.type === ";");
|
|
1825
|
+
const start = kwNode ? kwNode.endIndex - node.startIndex : keyword.length;
|
|
1826
|
+
const terminator = block ?? semi;
|
|
1827
|
+
const end = terminator ? terminator.startIndex - node.startIndex : node.text.length;
|
|
1828
|
+
if (end <= start) return "";
|
|
1829
|
+
return collapseWhitespace(node.text.slice(start, end));
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* The family of the `@font-face` block a declaration sits in, or `undefined`.
|
|
1833
|
+
*
|
|
1834
|
+
* Walks UP from the declaration to its enclosing `@font-face` and reads that
|
|
1835
|
+
* block's own `font-family` declaration — the same two-step the symbol push does,
|
|
1836
|
+
* and deliberately so: CSS binds a `font-feature-settings` value to the family of
|
|
1837
|
+
* the `@font-face` it appears in, so the edge target cannot be derived from the
|
|
1838
|
+
* value alone.
|
|
1839
|
+
*/
|
|
1840
|
+
fontFamilyOf(decl) {
|
|
1841
|
+
let n = decl;
|
|
1842
|
+
while (n) {
|
|
1843
|
+
if (n.type === "at_rule") {
|
|
1844
|
+
const kw = this.findChild(n, (c) => c?.type === "at_keyword");
|
|
1845
|
+
if (kw && kw.text.trim().toLowerCase() === "@font-face") {
|
|
1846
|
+
const block = this.findChild(n, (c) => c?.type === "block");
|
|
1847
|
+
const family = block ? this.findDescendant(block, (d) => {
|
|
1848
|
+
if (d.type !== "declaration") return false;
|
|
1849
|
+
const prop = this.findChild(d, (g) => g?.type === "property_name");
|
|
1850
|
+
return prop !== null && prop.text === "font-family";
|
|
1851
|
+
}) : null;
|
|
1852
|
+
if (family) {
|
|
1853
|
+
const value = this.findChild(family, (c) => c?.type === "plain_value" || c?.type === "string_value");
|
|
1854
|
+
if (value) return stripAllQuotes(value.text);
|
|
1855
|
+
}
|
|
1856
|
+
return void 0;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
n = n.parent;
|
|
1860
|
+
}
|
|
1861
|
+
return void 0;
|
|
1862
|
+
}
|
|
1863
|
+
fontFaceSymbolName(family) {
|
|
1864
|
+
return `@font-face ${family}`;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Namespace prefixes declared by `@namespace`, collected before the walk for the
|
|
1868
|
+
* same order-independence reason as keyframes and font faces: a selector may use a
|
|
1869
|
+
* prefix before the `@namespace` that declares it, and CSS does not care.
|
|
1870
|
+
*
|
|
1871
|
+
* The value is the PUBLISHED symbol name, so the edge and the symbol cannot name
|
|
1872
|
+
* different things — the same single-expression discipline as `@font-face`.
|
|
1873
|
+
*/
|
|
1874
|
+
collectNamespacePrefixes(root, ctx) {
|
|
1875
|
+
const stack = [root];
|
|
1876
|
+
while (stack.length > 0) {
|
|
1877
|
+
const n = stack.pop();
|
|
1878
|
+
if (this.isErrorNode(n)) continue;
|
|
1879
|
+
if (n.type === "namespace_statement") {
|
|
1880
|
+
const nameNode = this.findChild(n, (c) => c?.type === "namespace_name");
|
|
1881
|
+
if (nameNode) {
|
|
1882
|
+
const prefix = nameNode.text.trim();
|
|
1883
|
+
const kwNode = this.findChild(n, (c) => c?.type === "@namespace");
|
|
1884
|
+
const semi = this.findChild(n, (c) => c?.type === ";");
|
|
1885
|
+
const start = kwNode ? kwNode.endIndex - n.startIndex : "@namespace".length;
|
|
1886
|
+
const end = semi ? semi.startIndex - n.startIndex : n.text.length;
|
|
1887
|
+
const prelude = end <= start ? "" : collapseWhitespace(n.text.slice(start, end));
|
|
1888
|
+
if (prefix && prelude) ctx.namespacePrefixes[prefix] = `@namespace ${prelude}`;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1892
|
+
const c = n.namedChild(i);
|
|
1893
|
+
if (c) stack.push(c);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
collectKeyframeNames(root, ctx) {
|
|
1898
|
+
const stack = [root];
|
|
1899
|
+
while (stack.length > 0) {
|
|
1900
|
+
const n = stack.pop();
|
|
1901
|
+
if (this.isErrorNode(n)) continue;
|
|
1902
|
+
if (n.type === "keyframes_statement") {
|
|
1903
|
+
const nameNode = this.findChild(
|
|
1904
|
+
n,
|
|
1905
|
+
(c) => c && (c.type === "keyframes_name" || c.type === "identifier" || c.type === "string_value")
|
|
1906
|
+
);
|
|
1907
|
+
if (nameNode) ctx.keyframeNames[stripAllQuotes(nameNode.text)] = true;
|
|
1908
|
+
}
|
|
1909
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1910
|
+
const c = n.namedChild(i);
|
|
1911
|
+
if (c) stack.push(c);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Families declared by an `@font-face` in this file, collected BEFORE the walk for
|
|
1917
|
+
* the same reason `collectKeyframeNames` runs first: a rule may use a family
|
|
1918
|
+
* before the `@font-face` that declares it, and CSS does not care about order, so
|
|
1919
|
+
* an edge that resolved during the walk would depend on where the author happened
|
|
1920
|
+
* to put the declaration.
|
|
1921
|
+
*/
|
|
1922
|
+
/**
|
|
1923
|
+
* `@counter-style`, `@custom-media` and `@font-feature-values` are the last three
|
|
1924
|
+
* at-rules that DECLARE a named, referable thing, and until now all three were
|
|
1925
|
+
* dropped on the floor: the analyzer published no symbol for them, so a
|
|
1926
|
+
* `list-style: thumbs` had nothing to point at and a `@media (--narrow)` named
|
|
1927
|
+
* itself by raw text. That is the `@font-face` / `@property` defect class a
|
|
1928
|
+
* second time — and it survived because NO CORPUS FILE USES ANY OF THE THREE, so
|
|
1929
|
+
* they were classified NO OCCURRENCE and recorded as "not a defect". That is the
|
|
1930
|
+
* cost of that label: it means the corpus cannot speak about them, not that they
|
|
1931
|
+
* work. Three of three turned out to be broken.
|
|
1932
|
+
*
|
|
1933
|
+
* Pre-collected for the same order-independence reason as keyframes and font
|
|
1934
|
+
* faces: CSS does not care whether a use precedes its declaration.
|
|
1935
|
+
*/
|
|
1936
|
+
collectCustomDefinitionNames(root, ctx) {
|
|
1937
|
+
const stack = [root];
|
|
1938
|
+
while (stack.length > 0) {
|
|
1939
|
+
const n = stack.pop();
|
|
1940
|
+
if (this.isErrorNode(n)) continue;
|
|
1941
|
+
if (n.type === "at_rule") {
|
|
1942
|
+
const kwNode = this.findChild(n, (c) => c?.type === "at_keyword");
|
|
1943
|
+
const keyword = kwNode ? kwNode.text.trim().toLowerCase() : "";
|
|
1944
|
+
const prelude = this.atRulePrelude(n, keyword);
|
|
1945
|
+
if (keyword === "@counter-style" && prelude) ctx.counterStyleNames[prelude] = true;
|
|
1946
|
+
else if (keyword === "@custom-media") {
|
|
1947
|
+
const name = leadingCustomIdent(prelude);
|
|
1948
|
+
if (name) ctx.customMediaNames[name] = true;
|
|
1949
|
+
} else if (keyword === "@font-feature-values" && prelude) {
|
|
1950
|
+
ctx.fontFeatureValuesNames[prelude] = true;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1954
|
+
const c = n.namedChild(i);
|
|
1955
|
+
if (c) stack.push(c);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
collectFontFaceNames(root, ctx) {
|
|
1960
|
+
const stack = [root];
|
|
1961
|
+
while (stack.length > 0) {
|
|
1962
|
+
const n = stack.pop();
|
|
1963
|
+
if (this.isErrorNode(n)) continue;
|
|
1964
|
+
if (n.type === "at_rule") {
|
|
1965
|
+
const kw = this.findChild(n, (c) => c?.type === "at_keyword");
|
|
1966
|
+
if (kw && kw.text.trim() === "@font-face") {
|
|
1967
|
+
const block = this.findChild(n, (c) => c?.type === "block");
|
|
1968
|
+
const decl = block ? this.findDescendant(block, (d) => {
|
|
1969
|
+
if (d.type !== "declaration") return false;
|
|
1970
|
+
const prop = this.findChild(d, (g) => g?.type === "property_name");
|
|
1971
|
+
return prop !== null && prop.text.trim().toLowerCase() === "font-family";
|
|
1972
|
+
}) : null;
|
|
1973
|
+
if (decl) {
|
|
1974
|
+
const value = this.findChild(decl, (c) => c?.type === "string_value" || c?.type === "plain_value" || c?.type === "identifier");
|
|
1975
|
+
if (value) ctx.fontFaceNames[stripAllQuotes(value.text.trim())] = true;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1980
|
+
const c = n.namedChild(i);
|
|
1981
|
+
if (c) stack.push(c);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Publishes a `@keyframes` declaration as a symbol and returns the name it published, or
|
|
1987
|
+
* `""` when it published none.
|
|
1988
|
+
*
|
|
1989
|
+
* The RETURN is load-bearing and is new: the caller stores it as the owning scope for the
|
|
1990
|
+
* declarations inside, which is what lets a `var()` inside a `keyframe_block` be emitted
|
|
1991
|
+
* as a `references` edge from the animation instead of being dropped. See
|
|
1992
|
+
* `StackFrame.keyframeScope`. It returns the name rather than `void` so the two can never
|
|
1993
|
+
* disagree — a symbol published under a name the caller has to re-derive is the shape that
|
|
1994
|
+
* produced the bare-`@container` identity collision this lane already had to undo.
|
|
1995
|
+
*/
|
|
1996
|
+
processKeyframes(node, ctx) {
|
|
1997
|
+
const nameNode = this.findChild(
|
|
1998
|
+
node,
|
|
1999
|
+
(c) => c && (c.type === "keyframes_name" || c.type === "identifier" || c.type === "string_value")
|
|
2000
|
+
);
|
|
2001
|
+
if (!nameNode) return "";
|
|
2002
|
+
const name = stripAllQuotes(nameNode.text);
|
|
2003
|
+
ctx.symbols.push({
|
|
2004
|
+
name,
|
|
2005
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
2006
|
+
range: this.nodeToRange(node),
|
|
2007
|
+
scope: "",
|
|
2008
|
+
visibility: "public",
|
|
2009
|
+
complexity: 1 + this.countKeyframeStops(node),
|
|
2010
|
+
metadata: {
|
|
2011
|
+
isKeyframes: true
|
|
2012
|
+
// (the stop-count complexity is set on the symbol above)
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
ctx.keyframeNames[name] = true;
|
|
2016
|
+
ctx.patterns["CSS Animations"] = true;
|
|
2017
|
+
const keywordNode = this.findChild(node, (c) => c?.type === "at_keyword");
|
|
2018
|
+
const keyword = keywordNode ? keywordNode.text.trim().toLowerCase() : "@keyframes";
|
|
2019
|
+
this.recordKeyframesOverride(ctx, name, keyword, this.lineOf(node));
|
|
2020
|
+
return name;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* M14 ONTOLOGY, `overrides` — CSS's only inheritance-like construct, and the one the
|
|
2024
|
+
* lane was silently omitting.
|
|
2025
|
+
*
|
|
2026
|
+
* ## What the construct is
|
|
2027
|
+
*
|
|
2028
|
+
* CSS Animations L1 : "If multiple `@keyframes` rules with the same name are
|
|
2029
|
+
* declared, the last one wins." The Properties & Values API says the same for a
|
|
2030
|
+
* re-registered `@property --x`. So a second declaration of a name REPLACES the
|
|
2031
|
+
* first, wholly — the earlier block becomes dead code. This is the same shape the
|
|
2032
|
+
* gate's own header records for bash's 174 redefinition events, and the reason the
|
|
2033
|
+
* differential could not find it here: `deriveCssEdges` derives `contains`, `uses`,
|
|
2034
|
+
* `references` and `imports`, and both walkers said nothing about `overrides`.
|
|
2035
|
+
*
|
|
2036
|
+
* ## What the corpus says, and the number that changed the design
|
|
2037
|
+
*
|
|
2038
|
+
* `validation/m14/census/css-overrides-split.ts` over the whole admitted corpus
|
|
2039
|
+
* (1,114 files, 61 repos) found **146 second declarations of an already-declared
|
|
2040
|
+
* `@keyframes` name in one file** — and **142 of them are a vendor-prefixed pair**,
|
|
2041
|
+
* overwhelmingly Font Awesome's `@-webkit-keyframes fa-spin` / `@keyframes fa-spin`.
|
|
2042
|
+
* A prefixed pair is not a redeclaration: two animations, selected by support. An
|
|
2043
|
+
* `overrides` edge for each would have been a fabrication over 97.3% of the type, on
|
|
2044
|
+
* the strength of an aggregate count.
|
|
2045
|
+
*
|
|
2046
|
+
* The remaining **4** are the real thing: `hasura-engine/frontend/libs/console/
|
|
2047
|
+
* legacy-ce/src/lib/components/Services/ApiExplorer/GraphiQLWrapper/GraphiQL.css`
|
|
2048
|
+
* (`@keyframes rotation` at L1534 and again at L2214) and three in
|
|
2049
|
+
* `vue-element-admin/src/assets/custom-theme/index.css` (`v-modal-in`, `v-modal-out`,
|
|
2050
|
+
* `progress`). Four occurrences in 1,114 files is a thin census and it is reported as
|
|
2051
|
+
* one — but "four" is not "none", and a ruling of `overrides` that said the construct
|
|
2052
|
+
* does not occur would be false by exactly those four.
|
|
2053
|
+
*
|
|
2054
|
+
* THE AGGREGATE ALSO NEEDS A CORRECTION, and the correction is about the instrument
|
|
2055
|
+
* rather than the language. The first version of the split probe bucketed a whole
|
|
2056
|
+
* NAME-GROUP by whether the group mentioned more than one keyword, so `v-modal-in`
|
|
2057
|
+
* — spelled three times, once prefixed and twice not — was filed under
|
|
2058
|
+
* "cross-keyword" on the strength of its first pair and its genuine redefinition was
|
|
2059
|
+
* never counted. It reported 1 where the truth is 4. The rule that decides this is
|
|
2060
|
+
* adjacency, because CSS Animations L1 makes the LAST declaration the live one, so
|
|
2061
|
+
* a declaration is compared against the one it replaces and not against its whole
|
|
2062
|
+
* group. `validation/m14/census/css-overrides-split.ts` now counts adjacent pairs and
|
|
2063
|
+
* agrees with the production census at 4.
|
|
2064
|
+
*
|
|
2065
|
+
* `@property` re-registration is counted at **0** occurrences, because the corpus
|
|
2066
|
+
* cannot speak about it. It is implemented anyway and pinned by a fixture, which is
|
|
2067
|
+
* this lane's standing answer to a construct the corpus does not exercise (see the
|
|
2068
|
+
* `@counter-style` / `@custom-media` / `@font-feature-values` note in
|
|
2069
|
+
* `collectCustomDefinitionNames`): a measured zero means the corpus is silent, not
|
|
2070
|
+
* that the branch is wrong.
|
|
2071
|
+
*
|
|
2072
|
+
* ## Why `source === target`, stated rather than hidden
|
|
2073
|
+
*
|
|
2074
|
+
* The overriding declaration and the overridden one have the SAME NAME, and this
|
|
2075
|
+
* lane publishes ONE symbol per name — the merge at the end of `analyze` is keyed
|
|
2076
|
+
* `(kind, name)` and deliberately so, because "a selector is a MATCH PATTERN, not a
|
|
2077
|
+
* declaration" and the same reasoning makes two `@keyframes spin` blocks two
|
|
2078
|
+
* occurrences of one node. So the edge's two ends are the same string, and what
|
|
2079
|
+
* distinguishes them is the SITE: `line_number` is the superseding declaration's
|
|
2080
|
+
* line and `metadata.supersedesLine` is the declaration it kills.
|
|
2081
|
+
*
|
|
2082
|
+
* That is the shape `real-markdown-analyzer.ts:142` already guards for rather than
|
|
2083
|
+
* forbids — `if (source === target && !distinctSites) return`, whose own comment is
|
|
2084
|
+
* "The two ends name DIFFERENT sites even when they spell the same string". The two
|
|
2085
|
+
* declarations here are two blocks of source text on two lines, so they are two
|
|
2086
|
+
* sites by any reading, and `distinctSites: true` is recorded in the metadata
|
|
2087
|
+
* rather than left for a reader to infer.
|
|
2088
|
+
*
|
|
2089
|
+
* The line is also load-bearing for STORAGE, not only for meaning: a relationship's
|
|
2090
|
+
* store key is (source, type, target, line) — #822, which markdown's own
|
|
2091
|
+
* comment records — so an edge without the superseding line would collide with any
|
|
2092
|
+
* other `spin -> spin` edge in the file and `INSERT OR IGNORE` would keep one.
|
|
2093
|
+
*
|
|
2094
|
+
* ## Confidence
|
|
2095
|
+
*
|
|
2096
|
+
* 1.0. This is not a name-match guess: both ends are read from two `@keyframes`
|
|
2097
|
+
* nodes, the equality test is on the tree's own keyframes-name node, and the
|
|
2098
|
+
* keyword discriminator is the tree's own `at_keyword`. There is no inference in it
|
|
2099
|
+
* to be uncertain about.
|
|
2100
|
+
*/
|
|
2101
|
+
recordKeyframesOverride(ctx, name, keyword, line) {
|
|
2102
|
+
if (!name || !line) return;
|
|
2103
|
+
const live = ctx.keyframeLive[name];
|
|
2104
|
+
if (live === void 0) {
|
|
2105
|
+
ctx.keyframeLive[name] = { keyword, line };
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (live.keyword === keyword) {
|
|
2109
|
+
this.emitOverride(ctx, name, line, live.line, {
|
|
2110
|
+
isKeyframesRedefinition: true,
|
|
2111
|
+
atKeyword: keyword
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
ctx.keyframeLive[name] = { keyword, line };
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* `@property --x` written twice is the same last-wins replacement
|
|
2118
|
+
* (`recordKeyframesOverride`'s rule, from the Properties & Values API rather than
|
|
2119
|
+
* from CSS Animations). Measured at 0 occurrences over 1,114 corpus files, which is
|
|
2120
|
+
* a fact about the corpus and not about the construct — so it is pinned by a fixture
|
|
2121
|
+
* instead of being dropped for want of evidence.
|
|
2122
|
+
*/
|
|
2123
|
+
recordPropertyOverride(ctx, name, line) {
|
|
2124
|
+
if (!name || !line) return;
|
|
2125
|
+
const prior = ctx.propertyLive[name];
|
|
2126
|
+
if (prior !== void 0) {
|
|
2127
|
+
this.emitOverride(ctx, name, line, prior, { isPropertyReregistration: true });
|
|
2128
|
+
}
|
|
2129
|
+
ctx.propertyLive[name] = line;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* M14 ONTOLOGY PASS 2, `overrides` — a `--custom-property` assigned TWICE inside ONE
|
|
2133
|
+
* declaration block. Found by the pass-2 adversarial probe, not by reading: see
|
|
2134
|
+
* `validation/m14/census/css-boundary-probe.ts` (Q3), which counts the shape on the
|
|
2135
|
+
* parse tree of the whole admitted corpus.
|
|
2136
|
+
*
|
|
2137
|
+
* ## Why this is the same construct `@keyframes` already gets
|
|
2138
|
+
*
|
|
2139
|
+
* Two declarations of one property inside one block share origin, context, layer,
|
|
2140
|
+
* specificity and importance, so nothing about them distinguishes them except their
|
|
2141
|
+
* position, and CSS Cascade 5 resolves that: "The last declaration in document
|
|
2142
|
+
* order wins." CSS Variables 1 adds that custom properties are not special here —
|
|
2143
|
+
* "Custom properties are ordinary properties, so they can be declared on any element,
|
|
2144
|
+
* are resolved with the normal inheritance and cascade rules." So the second
|
|
2145
|
+
* assignment REPLACES the first outright and the first is dead, which is exactly what
|
|
2146
|
+
* `recordKeyframesOverride` asserts for a second `@keyframes` of one name.
|
|
2147
|
+
*
|
|
2148
|
+
* The corpus shape is Bulma's minified bundles, where `:root { --bulma-light-l: 90%;
|
|
2149
|
+
* … ; --bulma-light-l: 96%; … }` writes the same token twice inside one block.
|
|
2150
|
+
*
|
|
2151
|
+
* ## The block boundary is load-bearing, and narrower than it looks
|
|
2152
|
+
*
|
|
2153
|
+
* Only declarations in the SAME block produce an edge. `:root { --x: 1 }` followed by
|
|
2154
|
+
* `.dark { --x: 2 }` assigns `--x` twice in one FILE and is NOT an override: the two
|
|
2155
|
+
* apply to different elements and the cascade chooses between them per element, so an
|
|
2156
|
+
* edge claiming the second kills the first would be false. The block is therefore the
|
|
2157
|
+
* unit, tracked by `customPropBlockKey`, and the corpus's much larger
|
|
2158
|
+
* cross-block-assignment shape (which the same probe counts) is deliberately not
|
|
2159
|
+
* emitted. The rule is stated here because the boundary is the whole content of the
|
|
2160
|
+
* claim: "the last one wins" is only unconditional inside one block.
|
|
2161
|
+
*
|
|
2162
|
+
* ## `!important` is respected
|
|
2163
|
+
*
|
|
2164
|
+
* `--x: 1 !important; --x: 2;` does NOT replace — the important declaration wins and
|
|
2165
|
+
* the later one loses. Cascade 5 sorts importance above document order, so the
|
|
2166
|
+
* two are not interchangeable and an edge between them would be backwards. Only a pair
|
|
2167
|
+
* of EQUAL importance is emitted, which is the only case where "later wins" is the
|
|
2168
|
+
* whole rule. Measured on the corpus: every occurrence is an equal-importance pair, so
|
|
2169
|
+
* this guard costs nothing there and is recorded because a guard that costs nothing is
|
|
2170
|
+
* indistinguishable from a guard that was never needed.
|
|
2171
|
+
*/
|
|
2172
|
+
recordCustomPropertyOverride(node, name, ctx) {
|
|
2173
|
+
const line = this.lineOf(node);
|
|
2174
|
+
if (!name || !line) return;
|
|
2175
|
+
let block = node.parent;
|
|
2176
|
+
let key = `decl@${node.startIndex}`;
|
|
2177
|
+
while (block) {
|
|
2178
|
+
if (block.type === "rule_set" || block.type === "keyframe_block") {
|
|
2179
|
+
key = `${block.type}@${block.startIndex}`;
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
block = block.parent;
|
|
2183
|
+
}
|
|
2184
|
+
if (ctx.customPropBlockKey !== key) {
|
|
2185
|
+
ctx.customPropBlockKey = key;
|
|
2186
|
+
ctx.customPropInBlock = {};
|
|
2187
|
+
}
|
|
2188
|
+
const important = node.text.indexOf("!important") !== -1;
|
|
2189
|
+
const prior = ctx.customPropInBlock[name];
|
|
2190
|
+
if (prior && prior.important === important) {
|
|
2191
|
+
this.emitOverride(ctx, name, line, prior.line, {
|
|
2192
|
+
isCustomPropertyRedefinition: true,
|
|
2193
|
+
blockSelector: key
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
ctx.customPropInBlock[name] = { line, important };
|
|
2197
|
+
}
|
|
2198
|
+
emitOverride(ctx, name, laterLine, priorLine, extra) {
|
|
2199
|
+
const key = `${name}\0${laterLine}`;
|
|
2200
|
+
if (ctx.overrideEdges[key]) return;
|
|
2201
|
+
ctx.overrideEdges[key] = true;
|
|
2202
|
+
ctx.relationships.push({
|
|
2203
|
+
source: name,
|
|
2204
|
+
target: name,
|
|
2205
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
2206
|
+
// the name on this edge is the `@keyframes` name, and it is written on
|
|
2207
|
+
// BOTH lines — the superseding one first, because that is the declaration the
|
|
2208
|
+
// edge is about and the one a reader navigates to.
|
|
2209
|
+
line_number: laterLine,
|
|
2210
|
+
confidence: 1,
|
|
2211
|
+
metadata: {
|
|
2212
|
+
cssSpecific: true,
|
|
2213
|
+
isOverride: true,
|
|
2214
|
+
redefinition: true,
|
|
2215
|
+
// The declaration this one kills. Carried explicitly so a consumer does not
|
|
2216
|
+
// have to diff the file to find out WHICH earlier block died.
|
|
2217
|
+
supersedesLine: priorLine,
|
|
2218
|
+
// See the header: the two ends are two SITES, not one name twice.
|
|
2219
|
+
distinctSites: true,
|
|
2220
|
+
...extra
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
processSupports(node, ctx) {
|
|
2225
|
+
const queryNode = this.findChild(
|
|
2226
|
+
node,
|
|
2227
|
+
(c) => c && c.type !== "block" && c.type !== "at_keyword" && c.type !== "@supports" && c.text.trim() !== "@supports"
|
|
2228
|
+
);
|
|
2229
|
+
if (queryNode) {
|
|
2230
|
+
const condition = queryNode.text.trim();
|
|
2231
|
+
ctx.symbols.push({
|
|
2232
|
+
name: `@supports ${condition}`,
|
|
2233
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
2234
|
+
range: this.nodeToRange(node),
|
|
2235
|
+
scope: "",
|
|
2236
|
+
visibility: "public",
|
|
2237
|
+
complexity: this.queryComplexity(condition),
|
|
2238
|
+
metadata: {
|
|
2239
|
+
isSupports: true,
|
|
2240
|
+
supportsCondition: condition
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
ctx.patterns["Feature Queries (@supports)"] = true;
|
|
2244
|
+
return `@supports ${condition}`;
|
|
2245
|
+
}
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
processAtRule(node, currentSelector, ctx) {
|
|
2249
|
+
const atKeywordNode = this.findChild(node, (c) => c?.type === "at_keyword");
|
|
2250
|
+
let keyword = atKeywordNode ? atKeywordNode.text : "";
|
|
2251
|
+
if (!keyword) keyword = this.leadingAtKeyword(node.text);
|
|
2252
|
+
if (!keyword) return {};
|
|
2253
|
+
let namePart = "";
|
|
2254
|
+
const DECLARING = /* @__PURE__ */ new Set([
|
|
2255
|
+
"@layer",
|
|
2256
|
+
"@container",
|
|
2257
|
+
"@scope",
|
|
2258
|
+
"@font-face",
|
|
2259
|
+
"@property",
|
|
2260
|
+
"@namespace",
|
|
2261
|
+
"@mixin",
|
|
2262
|
+
// CSS Functions and Mixins Level 1: `@function --name(a, b) { … }` DECLARES a
|
|
2263
|
+
// callable that `--name(1, 2)` invokes. Measured on the corpus: 10 `@function`
|
|
2264
|
+
// at-rules (all in .sass utility files under Documenter.jl), so the construct is
|
|
2265
|
+
// real in this lane's population and arrives as a generic `at_rule`.
|
|
2266
|
+
"@function",
|
|
2267
|
+
// the three that were silently dropped: each DECLARES a named, referable
|
|
2268
|
+
// thing, so each belongs here. Measured, not assumed — a probe of all three
|
|
2269
|
+
// returned zero symbols before this line and the fixtures pin them now.
|
|
2270
|
+
"@counter-style",
|
|
2271
|
+
"@custom-media",
|
|
2272
|
+
"@font-feature-values"
|
|
2273
|
+
]);
|
|
2274
|
+
const CONSUMING = /* @__PURE__ */ new Set(["@tailwind", "@apply", "@theme", "@config", "@include", "@use", "@forward"]);
|
|
2275
|
+
if (DECLARING.has(keyword) || CONSUMING.has(keyword)) {
|
|
2276
|
+
let namePartOverride = "";
|
|
2277
|
+
if (keyword === "@font-face") {
|
|
2278
|
+
const block = this.findChild(node, (c) => c?.type === "block");
|
|
2279
|
+
const family = block ? this.findDescendant(block, (n) => {
|
|
2280
|
+
if (n.type !== "declaration") return false;
|
|
2281
|
+
const prop = this.findChild(n, (g) => g?.type === "property_name");
|
|
2282
|
+
return prop !== null && prop.text === "font-family";
|
|
2283
|
+
}) : null;
|
|
2284
|
+
if (family) {
|
|
2285
|
+
const value = this.findChild(family, (c) => c?.type === "plain_value" || c?.type === "string_value");
|
|
2286
|
+
if (value) namePartOverride = collapseWhitespace(stripAllQuotes(value.text));
|
|
2287
|
+
void namePartOverride;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const afterKeyword = node.text.slice(keyword.length);
|
|
2291
|
+
const braceAt = afterKeyword.indexOf("{");
|
|
2292
|
+
const semiAt = afterKeyword.indexOf(";");
|
|
2293
|
+
let cut = afterKeyword.length;
|
|
2294
|
+
if (braceAt !== -1) cut = braceAt;
|
|
2295
|
+
if (semiAt !== -1 && semiAt < cut) cut = semiAt;
|
|
2296
|
+
const namePart2 = namePartOverride || stripAllQuotes(collapseWhitespace(afterKeyword.slice(0, cut)));
|
|
2297
|
+
const ruleName = keyword === "@font-face" && namePart2 ? this.fontFaceSymbolName(namePart2) : `${keyword}${namePart2 ? " " + namePart2 : ""}`;
|
|
2298
|
+
if (keyword === "@layer" && !this.findChild(node, (c) => c?.type === "block")) {
|
|
2299
|
+
const names = namePart2.split(",").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
2300
|
+
for (const nm of names) {
|
|
2301
|
+
ctx.layerOrder.push(nm);
|
|
2302
|
+
if (ctx.layerNames[nm]) continue;
|
|
2303
|
+
ctx.layerNames[nm] = true;
|
|
2304
|
+
ctx.symbols.push({
|
|
2305
|
+
name: `${keyword} ${nm}`,
|
|
2306
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
2307
|
+
range: this.nodeToRange(node),
|
|
2308
|
+
scope: "",
|
|
2309
|
+
visibility: "public",
|
|
2310
|
+
complexity: 1,
|
|
2311
|
+
metadata: {
|
|
2312
|
+
isAtRule: true,
|
|
2313
|
+
atKeyword: keyword,
|
|
2314
|
+
isLayerOrdering: true,
|
|
2315
|
+
// The cascade position this statement gives the layer. Per-SYMBOL
|
|
2316
|
+
// rather than per-file, and that is not a style choice: `parseFile`
|
|
2317
|
+
// rebuilds the file metadata as a fresh literal listing specific keys
|
|
2318
|
+
// (engine.ts:1572, whose own comment records that anything it omits is
|
|
2319
|
+
// "computed and DISCARDED — silently"), so a file-level key added here
|
|
2320
|
+
// would never reach a consumer. Symbol metadata is passed through, and
|
|
2321
|
+
// the position is a property of the LAYER anyway.
|
|
2322
|
+
layerIndex: ctx.layerOrder.length - 1
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return {};
|
|
2327
|
+
}
|
|
2328
|
+
if (DECLARING.has(keyword)) {
|
|
2329
|
+
const publishedName = keyword === "@property" ? namePart2 || ruleName : keyword === "@custom-media" ? (
|
|
2330
|
+
// `namePart` is the PRELUDE; `ruleName` is the keyword plus the
|
|
2331
|
+
// prelude, so slicing the latter matches nothing and the first
|
|
2332
|
+
// version published `@custom-media --narrow (max-width: 400px)` —
|
|
2333
|
+
// a name the `@media (--narrow)` can never resolve against.
|
|
2334
|
+
leadingCustomIdent(namePart2) || ruleName
|
|
2335
|
+
) : ruleName;
|
|
2336
|
+
if (keyword === "@layer" && namePart2) {
|
|
2337
|
+
if (ctx.layerNames[namePart2]) {
|
|
2338
|
+
const existing = ctx.symbols.find((sy) => sy.name === `${keyword} ${namePart2}`);
|
|
2339
|
+
if (existing) {
|
|
2340
|
+
const em = existing.metadata ?? {};
|
|
2341
|
+
const blockRange = this.nodeToRange(node);
|
|
2342
|
+
if (em.hasLayerBlock !== true) {
|
|
2343
|
+
existing.range = blockRange;
|
|
2344
|
+
existing.metadata = { ...em, hasLayerBlock: true, scopeBlocks: 1 };
|
|
2345
|
+
} else {
|
|
2346
|
+
existing.range = {
|
|
2347
|
+
...existing.range,
|
|
2348
|
+
endLine: Math.max(existing.range?.endLine ?? 0, blockRange.endLine)
|
|
2349
|
+
};
|
|
2350
|
+
const n = typeof em.scopeBlocks === "number" ? em.scopeBlocks : 1;
|
|
2351
|
+
existing.metadata = { ...em, scopeBlocks: n + 1 };
|
|
2352
|
+
}
|
|
2353
|
+
ctx.scopeSymbol[`${keyword} ${namePart2}`] = existing;
|
|
2354
|
+
}
|
|
2355
|
+
return { layer: `${keyword} ${namePart2}` };
|
|
2356
|
+
}
|
|
2357
|
+
ctx.layerNames[namePart2] = true;
|
|
2358
|
+
}
|
|
2359
|
+
const atRuleSymbol = {
|
|
2360
|
+
name: publishedName,
|
|
2361
|
+
// A declared `@font-face` and a registered `@property` are both
|
|
2362
|
+
// declarations of a named thing, which is what `variable` means in this
|
|
2363
|
+
// ontology; `@mixin` is a callable, so `function`; the rest are
|
|
2364
|
+
// structural scopes, which is `module`.
|
|
2365
|
+
kind: keyword === "@mixin" ? ASTEngine.SymbolKind.Function : keyword === "@font-face" || keyword === "@property" || keyword === "@counter-style" || keyword === "@custom-media" || keyword === "@font-feature-values" ? ASTEngine.SymbolKind.Variable : ASTEngine.SymbolKind.Module,
|
|
2366
|
+
range: this.nodeToRange(node),
|
|
2367
|
+
scope: "",
|
|
2368
|
+
visibility: "public",
|
|
2369
|
+
complexity: 1 + this.countDescriptors(node),
|
|
2370
|
+
metadata: {
|
|
2371
|
+
isAtRule: true,
|
|
2372
|
+
atKeyword: keyword,
|
|
2373
|
+
// the keyword is dropped from a `@property` symbol's NAME, so it is
|
|
2374
|
+
// recorded here — the declaration is still identifiable as a
|
|
2375
|
+
// registration rather than a rule-level declaration
|
|
2376
|
+
...keyword === "@property" ? { isPropertyRegistration: true } : {},
|
|
2377
|
+
// the three late additions carry which construct declared them, because
|
|
2378
|
+
// a `@custom-media` symbol is a BARE `--x` name — identical in shape to
|
|
2379
|
+
// a registered `@property` symbol and to a rule-level custom property,
|
|
2380
|
+
// so without this the three declarations are indistinguishable in the
|
|
2381
|
+
// graph
|
|
2382
|
+
...keyword === "@counter-style" ? { isCounterStyleDeclaration: true } : {},
|
|
2383
|
+
...keyword === "@custom-media" ? { isCustomMediaDeclaration: true } : {},
|
|
2384
|
+
...keyword === "@font-feature-values" ? { isFontFeatureValuesDeclaration: true } : {}
|
|
2385
|
+
// (the descriptor-count complexity is set on the symbol above)
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
ctx.symbols.push(atRuleSymbol);
|
|
2389
|
+
if (STRUCTURAL_SCOPE_AT_RULES.has(keyword)) ctx.scopeSymbol[publishedName] = atRuleSymbol;
|
|
2390
|
+
if (keyword === "@property") {
|
|
2391
|
+
this.recordPropertyOverride(ctx, publishedName, this.lineOf(node));
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (keyword === "@layer") ctx.patterns["Cascade Layers (@layer)"] = true;
|
|
2395
|
+
if (keyword === "@container") ctx.patterns["Container Queries (@container)"] = true;
|
|
2396
|
+
if (keyword === "@tailwind" || keyword === "@apply" || keyword === "@theme" || keyword === "@config")
|
|
2397
|
+
ctx.frameworks["Tailwind CSS"] = true;
|
|
2398
|
+
if (keyword === "@mixin" || keyword === "@include" || keyword === "@forward" || keyword === "@use") {
|
|
2399
|
+
ctx.frameworks["SCSS/SASS"] = true;
|
|
2400
|
+
ctx.patterns["CSS Preprocessor Macros"] = true;
|
|
2401
|
+
}
|
|
2402
|
+
if (keyword === "@mixin" && namePart2) ctx.mixinNames[namePart2] = true;
|
|
2403
|
+
if (keyword === "@function" && namePart2) {
|
|
2404
|
+
const callable = firstCustomFunctionName(namePart2);
|
|
2405
|
+
if (callable) ctx.cssFunctionNames[callable] = `${keyword} ${namePart2}`;
|
|
2406
|
+
}
|
|
2407
|
+
if (keyword === "@include" && currentSelector && namePart2) {
|
|
2408
|
+
ctx.relationships.push({
|
|
2409
|
+
source: currentSelector,
|
|
2410
|
+
target: `@mixin ${namePart2}`,
|
|
2411
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2412
|
+
// the referenced mixin name is written in the `@include`
|
|
2413
|
+
// prelude, on the at-rule's own line. (The relation stays `uses` — a
|
|
2414
|
+
// mixin reference is a USE. An edit here briefly changed it to
|
|
2415
|
+
// `contains` on the theory that the mixin is "contained" in the rule;
|
|
2416
|
+
// that is a different claim and a wrong one, and it is reverted.)
|
|
2417
|
+
line_number: this.lineOf(node),
|
|
2418
|
+
metadata: { cssSpecific: true, isMixinRef: true, isDeclaredLocally: ctx.mixinNames[namePart2] === true }
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
if ((keyword === "@use" || keyword === "@forward") && namePart2) {
|
|
2422
|
+
ctx.relationships.push({
|
|
2423
|
+
source: this.importerName,
|
|
2424
|
+
target: stripAllQuotes(firstToken(namePart2)),
|
|
2425
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
2426
|
+
// the module name is the at-rule's prelude, on its own line.
|
|
2427
|
+
line_number: this.lineOf(node),
|
|
2428
|
+
confidence: 1,
|
|
2429
|
+
metadata: { isCssImport: true, isSassModuleImport: true, importSource: this.importerName }
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
layer: keyword === "@layer" ? ruleName : void 0,
|
|
2434
|
+
container: keyword === "@container" ? ruleName : void 0,
|
|
2435
|
+
// A `@scope` prelude is a SELECTOR (`(.a) to (.b)`), not a condition, so
|
|
2436
|
+
// it cannot reuse the media field's name. It is reported as its own
|
|
2437
|
+
// scope so a rule inside it is attributable to the scope that contains
|
|
2438
|
+
// it, which is the whole point of the construct.
|
|
2439
|
+
scope: keyword === "@scope" ? ruleName : void 0
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
return {};
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Every property a selector declares, recorded on the SELECTOR SYMBOL rather
|
|
2446
|
+
* than emitted as a relationship to the property name.
|
|
2447
|
+
*
|
|
2448
|
+
* RULING . This used to emit `uses <selector> -> <property>` for
|
|
2449
|
+
* every declaration, and the class table says what that cost:
|
|
2450
|
+
*
|
|
2451
|
+
* uses 34,727 edges 2,073 resolve at both ends 6.0%
|
|
2452
|
+
* of which 32,654 have a RESOLVABLE SOURCE and an unresolvable target,
|
|
2453
|
+
* and 0 have an unresolvable source
|
|
2454
|
+
*
|
|
2455
|
+
* The class is homogeneous: the source is always a selector (always a
|
|
2456
|
+
* published symbol) and the target is always a CSS property name — `color`,
|
|
2457
|
+
* `margin`, `font-family` — which the analyzer never publishes as a symbol and
|
|
2458
|
+
* never will, because a property name is a token in a declaration, not an
|
|
2459
|
+
* entity a stylesheet defines. So 32,654 of 59,940 relationships (54%) were
|
|
2460
|
+
* edges to a name that exists nowhere in the graph: stored as
|
|
2461
|
+
* `target_external`, unjoinable at query, and then discarded outright by
|
|
2462
|
+
* pool-worker's CSS branch of `filterNoiseRelationships`, which keeps
|
|
2463
|
+
* `contains` only. The information was paid for twice and delivered neither
|
|
2464
|
+
* time.
|
|
2465
|
+
*
|
|
2466
|
+
* A relationship is a claim about two graph nodes. `declaresProperties` is the
|
|
2467
|
+
* right shape for "this rule sets these properties" — it rides the symbol
|
|
2468
|
+
* payload, which M60 P1 treats as a first-class channel (produced by the
|
|
2469
|
+
* analyzer, preserved by the store, reachable at query, truthful at the tool).
|
|
2470
|
+
*
|
|
2471
|
+
* The relationships that DO survive in this handler are the ones whose target
|
|
2472
|
+
* is a real node: a custom property, a `@keyframes` name, a `@mixin`. Those
|
|
2473
|
+
* are untouched.
|
|
2474
|
+
*/
|
|
2475
|
+
recordDeclaredProperty(currentSelector, property, ctx) {
|
|
2476
|
+
if (!currentSelector) return;
|
|
2477
|
+
ctx.declaredProperties[currentSelector] = ctx.declaredProperties[currentSelector] ?? [];
|
|
2478
|
+
const list = ctx.declaredProperties[currentSelector];
|
|
2479
|
+
if (list.indexOf(property) === -1) list.push(property);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* CSS MODULES `export` SURFACE — `:export { name: value; }`.
|
|
2483
|
+
*
|
|
2484
|
+
* CSS Modules writes a cross-file export surface with no export syntax: a rule whose
|
|
2485
|
+
* selector is `:export` holds the names the module publishes to its importers. The
|
|
2486
|
+
* construct EXISTS in the language family and occurs 0 times in this lane's .css
|
|
2487
|
+
* population, so the type is implemented and counted once as corpus-unexercised —
|
|
2488
|
+
* a zero-sites ruling would otherwise sit in the denominator claiming nothing.
|
|
2489
|
+
*
|
|
2490
|
+
* Each declaration inside the block publishes a SYMBOL for the exported name and an
|
|
2491
|
+
* `exports` edge from the file's own module symbol to it, so the target is a symbol
|
|
2492
|
+
* this file really declares rather than a string a reader cannot join against.
|
|
2493
|
+
*/
|
|
2494
|
+
processCssModulesExport(node, entry, currentSelector, ctx) {
|
|
2495
|
+
if (currentSelector.trim() !== ":export") return false;
|
|
2496
|
+
const propNode = this.findChild(entry, (c) => c?.type === "property_name");
|
|
2497
|
+
if (!propNode) return false;
|
|
2498
|
+
const exported = stripAllQuotes(propNode.text).trim();
|
|
2499
|
+
if (exported.length === 0) return false;
|
|
2500
|
+
const range = this.nodeToRange(propNode);
|
|
2501
|
+
ctx.symbols.push({
|
|
2502
|
+
name: exported,
|
|
2503
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
2504
|
+
range,
|
|
2505
|
+
scope: "",
|
|
2506
|
+
visibility: "public",
|
|
2507
|
+
complexity: 1,
|
|
2508
|
+
metadata: { isCssModulesExport: true, exportedName: exported }
|
|
2509
|
+
});
|
|
2510
|
+
ctx.relationships.push({
|
|
2511
|
+
source: this.importerName,
|
|
2512
|
+
target: exported,
|
|
2513
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
2514
|
+
line_number: this.lineOf(propNode),
|
|
2515
|
+
confidence: 1,
|
|
2516
|
+
metadata: {
|
|
2517
|
+
cssSpecific: true,
|
|
2518
|
+
exportForm: "css_modules_export_block",
|
|
2519
|
+
isModuleSurface: true,
|
|
2520
|
+
evidenceClass: "structural"
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
return true;
|
|
2524
|
+
}
|
|
2525
|
+
processDeclaration(node, currentSelector, ctx) {
|
|
2526
|
+
if (this.processCssModulesExport(node, node, currentSelector, ctx)) return;
|
|
2527
|
+
const declPropNode = this.findChild(node, (c) => c.type === "property_name");
|
|
2528
|
+
if (declPropNode) this.recordDeclaredProperty(currentSelector, declPropNode.text, ctx);
|
|
2529
|
+
const propNode = this.findChild(node, (c) => c?.type === "property_name");
|
|
2530
|
+
if (!propNode) return;
|
|
2531
|
+
const propName = propNode.text;
|
|
2532
|
+
if (propName === "composes") {
|
|
2533
|
+
ctx.patterns["CSS Modules (composes)"] = true;
|
|
2534
|
+
ctx.frameworks["CSS Modules"] = true;
|
|
2535
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2536
|
+
const val = node.namedChild(i);
|
|
2537
|
+
if (!val || val.type === "property_name") continue;
|
|
2538
|
+
const target = collapseWhitespace(val.text);
|
|
2539
|
+
if (!target) continue;
|
|
2540
|
+
ctx.relationships.push({
|
|
2541
|
+
source: currentSelector,
|
|
2542
|
+
target,
|
|
2543
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2544
|
+
// the name is the composed class, so the line is the VALUE
|
|
2545
|
+
// node it is written on — not the `composes` property, which is a
|
|
2546
|
+
// different name on the same declaration.
|
|
2547
|
+
line_number: this.lineOf(val),
|
|
2548
|
+
metadata: { cssSpecific: true, isComposition: true }
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (propName.startsWith("--")) {
|
|
2553
|
+
ctx.symbols.push({
|
|
2554
|
+
name: propName,
|
|
2555
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
2556
|
+
range: this.nodeToRange(node),
|
|
2557
|
+
scope: currentSelector,
|
|
2558
|
+
visibility: "public",
|
|
2559
|
+
complexity: this.customPropertyComplexity(node),
|
|
2560
|
+
metadata: {
|
|
2561
|
+
isCustomProperty: true
|
|
2562
|
+
// (the var()-read complexity is set on the symbol above)
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
if (currentSelector) {
|
|
2566
|
+
ctx.relationships.push({
|
|
2567
|
+
source: currentSelector,
|
|
2568
|
+
target: propName,
|
|
2569
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
2570
|
+
// the name is the custom property, so the line is its
|
|
2571
|
+
// `property_name` node — NOT the `declaration`, which can open on
|
|
2572
|
+
// an earlier line (a leading comment inside the block, or a declaration
|
|
2573
|
+
// written across lines).
|
|
2574
|
+
line_number: this.lineOf(propNode),
|
|
2575
|
+
confidence: 1,
|
|
2576
|
+
metadata: { cssSpecific: true, isCustomPropertyScope: true }
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
this.recordCustomPropertyOverride(node, propName, ctx);
|
|
2580
|
+
ctx.patterns["CSS Custom Properties"] = true;
|
|
2581
|
+
}
|
|
2582
|
+
if ((propName === "list-style" || propName === "list-style-type") && currentSelector) {
|
|
2583
|
+
const nameStack = [node];
|
|
2584
|
+
while (nameStack.length > 0) {
|
|
2585
|
+
const n = nameStack.pop();
|
|
2586
|
+
if (n.type === "plain_value" || n.type === "identifier" || n.type === "string_value") {
|
|
2587
|
+
const style = n.text.trim();
|
|
2588
|
+
if (style.length > 0 && ctx.counterStyleNames[style]) {
|
|
2589
|
+
ctx.relationships.push({
|
|
2590
|
+
source: currentSelector,
|
|
2591
|
+
target: `@counter-style ${style}`,
|
|
2592
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2593
|
+
line_number: this.lineOf(n),
|
|
2594
|
+
metadata: { cssSpecific: true, isCounterStyleRef: true }
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2599
|
+
const c = n.namedChild(i);
|
|
2600
|
+
if (c) nameStack.push(c);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (propName === "font-feature-settings") {
|
|
2605
|
+
const enclosing = this.fontFamilyOf(node);
|
|
2606
|
+
if (enclosing && ctx.fontFeatureValuesNames[enclosing]) {
|
|
2607
|
+
const valueStack = [node];
|
|
2608
|
+
while (valueStack.length > 0) {
|
|
2609
|
+
const n = valueStack.pop();
|
|
2610
|
+
if (n.type === "string_value" || n.type === "plain_value") {
|
|
2611
|
+
ctx.relationships.push({
|
|
2612
|
+
source: this.fontFaceSymbolName(enclosing),
|
|
2613
|
+
target: `@font-feature-values ${enclosing}`,
|
|
2614
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2615
|
+
line_number: this.lineOf(n),
|
|
2616
|
+
metadata: { cssSpecific: true, isFontFeatureValuesRef: true }
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2620
|
+
const c = n.namedChild(i);
|
|
2621
|
+
if (c) valueStack.push(c);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (propName === "font-family" && currentSelector) {
|
|
2627
|
+
const famStack = [node];
|
|
2628
|
+
while (famStack.length > 0) {
|
|
2629
|
+
const n = famStack.pop();
|
|
2630
|
+
if (n.type === "plain_value" || n.type === "identifier" || n.type === "string_value") {
|
|
2631
|
+
const family = stripAllQuotes(n.text.trim());
|
|
2632
|
+
if (family.length > 0 && ctx.fontFaceNames[family]) {
|
|
2633
|
+
ctx.relationships.push({
|
|
2634
|
+
source: currentSelector,
|
|
2635
|
+
// The edge names the DECLARATION, which is the published symbol — not
|
|
2636
|
+
// the bare family, which nothing publishes. A resolvable endpoint, and
|
|
2637
|
+
// the same both-ends-must-resolve rule the chain edges follow.
|
|
2638
|
+
target: this.fontFaceSymbolName(family),
|
|
2639
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2640
|
+
// , the same rule as the animation edge: the name is the FAMILY
|
|
2641
|
+
// as written in the value, so the line is the value's line — not the
|
|
2642
|
+
// `font-family` property's, and for a fallback list
|
|
2643
|
+
// (`"Inter", sans-serif`) the specific family that matched rather
|
|
2644
|
+
// than the first token on the line.
|
|
2645
|
+
line_number: this.lineOf(n),
|
|
2646
|
+
metadata: { cssSpecific: true, isFontFamilyRef: true }
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2651
|
+
const child = n.namedChild(i);
|
|
2652
|
+
if (child) famStack.push(child);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
if ((propName === "animation" || propName === "animation-name") && currentSelector) {
|
|
2657
|
+
const valStack = [node];
|
|
2658
|
+
while (valStack.length > 0) {
|
|
2659
|
+
const n = valStack.pop();
|
|
2660
|
+
if (n.type === "plain_value" || n.type === "identifier" || n.type === "string_value") {
|
|
2661
|
+
const text = stripAllQuotes(n.text.trim());
|
|
2662
|
+
if (ctx.keyframeNames[text]) {
|
|
2663
|
+
ctx.relationships.push({
|
|
2664
|
+
source: currentSelector,
|
|
2665
|
+
target: text,
|
|
2666
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2667
|
+
// the name is the KEYFRAMES name, which is written in the
|
|
2668
|
+
// VALUE — not the `animation` property, which is a different name
|
|
2669
|
+
// and, for the shorthand, shares its line with the duration and
|
|
2670
|
+
// easing. Taking the value node is what makes the edge point at the
|
|
2671
|
+
// line a reader would call the reference.
|
|
2672
|
+
line_number: this.lineOf(n),
|
|
2673
|
+
metadata: { cssSpecific: true, isAnimationRef: true }
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2678
|
+
const child = n.namedChild(i);
|
|
2679
|
+
if (child) valStack.push(child);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (node.text.indexOf("!important") !== -1) {
|
|
2684
|
+
ctx.patterns["Uses !important"] = true;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* `keyframeScope` is the nearest enclosing `@keyframes` name, or `""`. A `var()` whose
|
|
2689
|
+
* owner is a keyframe block rather than a rule is emitted from THAT name; see
|
|
2690
|
+
* `StackFrame.keyframeScope` for the measurement that made it necessary.
|
|
2691
|
+
*/
|
|
2692
|
+
processCallExpression(node, currentSelector, keyframeScope, ctx) {
|
|
2693
|
+
const fnNameNode = this.findChild(node, (c) => c?.type === "function_name" || c?.type === "identifier");
|
|
2694
|
+
if (!fnNameNode) return;
|
|
2695
|
+
{
|
|
2696
|
+
const callee = fnNameNode.text.trim();
|
|
2697
|
+
const declared = ctx.cssFunctionNames[callee];
|
|
2698
|
+
if (declared) {
|
|
2699
|
+
const owner = currentSelector || keyframeScope || declared;
|
|
2700
|
+
ctx.relationships.push({
|
|
2701
|
+
source: owner,
|
|
2702
|
+
target: declared,
|
|
2703
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
2704
|
+
// the CALLEE node's line: the name is what the edge names, and a wrapped call's
|
|
2705
|
+
// statement line is a different line.
|
|
2706
|
+
line_number: this.lineOf(fnNameNode),
|
|
2707
|
+
confidence: 1,
|
|
2708
|
+
metadata: {
|
|
2709
|
+
cssSpecific: true,
|
|
2710
|
+
isUserDefinedFunctionCall: true,
|
|
2711
|
+
callableName: callee,
|
|
2712
|
+
evidenceClass: "structural"
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (fnNameNameIsVar(fnNameNode) && (currentSelector || keyframeScope)) {
|
|
2718
|
+
const argsNode = this.findChild(node, (c) => c?.type === "arguments");
|
|
2719
|
+
if (argsNode) {
|
|
2720
|
+
const owner = currentSelector || keyframeScope;
|
|
2721
|
+
const ownedByKeyframes = currentSelector === "" && keyframeScope !== "";
|
|
2722
|
+
const argStack = [argsNode];
|
|
2723
|
+
while (argStack.length > 0) {
|
|
2724
|
+
const n = argStack.pop();
|
|
2725
|
+
if (n.namedChildCount === 0 && n.text.trim().startsWith("--")) {
|
|
2726
|
+
ctx.relationships.push({
|
|
2727
|
+
source: owner,
|
|
2728
|
+
target: n.text.trim(),
|
|
2729
|
+
type: ASTEngine.RelationshipType.References,
|
|
2730
|
+
// the name is the custom property INSIDE `var()`, so the
|
|
2731
|
+
// line is that argument node. The `call_expression` starts at its
|
|
2732
|
+
// receiver's position, which for `calc(var(--x) * 2)` is the
|
|
2733
|
+
// `calc` — a different line whenever the value wraps.
|
|
2734
|
+
line_number: this.lineOf(n),
|
|
2735
|
+
// A parse fact, written explicitly rather than left absent: the owner is a
|
|
2736
|
+
// node this walk published and the target is an argument node of a `var()`
|
|
2737
|
+
// whose callee node reads `var`. Nothing here is matched by name against a
|
|
2738
|
+
// declared target — that is what `uses` does, and it is why those edges are
|
|
2739
|
+
// the ones that need a lower confidence. `isVariableRef`'s 161,768 siblings
|
|
2740
|
+
// write no confidence at all, which the lane's own census counts as a
|
|
2741
|
+
// distinct claim; this branch states its own instead of inheriting the
|
|
2742
|
+
// absence.
|
|
2743
|
+
...ownedByKeyframes ? { confidence: 1 } : {},
|
|
2744
|
+
metadata: {
|
|
2745
|
+
cssSpecific: true,
|
|
2746
|
+
isVariableRef: true,
|
|
2747
|
+
...ownedByKeyframes ? { isKeyframeScopeRef: true } : {}
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
ctx.patterns["CSS Custom Properties"] = true;
|
|
2751
|
+
}
|
|
2752
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2753
|
+
const child = n.namedChild(i);
|
|
2754
|
+
if (child) argStack.push(child);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
detectFrameworksFromClasses(ctx) {
|
|
2761
|
+
const selectors = [];
|
|
2762
|
+
for (let i = 0; i < ctx.symbols.length; i++) {
|
|
2763
|
+
if (ctx.symbols[i].metadata?.isSelector) selectors.push(ctx.symbols[i]);
|
|
2764
|
+
}
|
|
2765
|
+
let tailwindScore = 0;
|
|
2766
|
+
let bootstrapScore = 0;
|
|
2767
|
+
let muiScore = 0;
|
|
2768
|
+
let vuetifyScore = 0;
|
|
2769
|
+
let bulmaScore = 0;
|
|
2770
|
+
let foundationScore = 0;
|
|
2771
|
+
for (let i = 0; i < selectors.length; i++) {
|
|
2772
|
+
const s = selectors[i];
|
|
2773
|
+
const classNames = s.metadata?.classNames || [];
|
|
2774
|
+
for (let j = 0; j < classNames.length; j++) {
|
|
2775
|
+
const cls = classNames[j];
|
|
2776
|
+
if (isTailwindClass(cls)) tailwindScore += 5;
|
|
2777
|
+
if (isBootstrapClass(cls)) bootstrapScore += 5;
|
|
2778
|
+
if (cls.startsWith("Mui")) muiScore += 5;
|
|
2779
|
+
if (cls.startsWith("v-btn") || cls.startsWith("v-card") || cls.startsWith("v-col")) vuetifyScore += 5;
|
|
2780
|
+
if (isBulmaClass(cls)) bulmaScore += 5;
|
|
2781
|
+
if (isFoundationClass(cls)) foundationScore += 5;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if (tailwindScore > 10) ctx.frameworks["Tailwind CSS"] = true;
|
|
2785
|
+
if (bootstrapScore > 0) ctx.frameworks["Bootstrap / Grid System"] = true;
|
|
2786
|
+
if (muiScore > 5) ctx.frameworks["Material UI (MUI)"] = true;
|
|
2787
|
+
if (vuetifyScore > 5) ctx.frameworks["Vuetify"] = true;
|
|
2788
|
+
if (bulmaScore > 10) ctx.frameworks["Bulma"] = true;
|
|
2789
|
+
if (foundationScore > 10) ctx.frameworks["Foundation"] = true;
|
|
2790
|
+
}
|
|
2791
|
+
calculateMetrics(ctx) {
|
|
2792
|
+
let maintainability = 100;
|
|
2793
|
+
let performance = 100;
|
|
2794
|
+
const selectors = [];
|
|
2795
|
+
for (let i = 0; i < ctx.symbols.length; i++) {
|
|
2796
|
+
if (ctx.symbols[i].metadata?.isSelector) selectors.push(ctx.symbols[i]);
|
|
2797
|
+
}
|
|
2798
|
+
const complexSelectors = [];
|
|
2799
|
+
for (let i = 0; i < selectors.length; i++) {
|
|
2800
|
+
if (selectors[i].metadata?.isComplex) complexSelectors.push(selectors[i]);
|
|
2801
|
+
}
|
|
2802
|
+
if (selectors.length > 0) {
|
|
2803
|
+
const complexRatio = complexSelectors.length / selectors.length;
|
|
2804
|
+
if (complexRatio > 0.3) {
|
|
2805
|
+
maintainability -= 20;
|
|
2806
|
+
performance -= 10;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (ctx.patterns["Uses !important"]) {
|
|
2810
|
+
maintainability -= 25;
|
|
2811
|
+
}
|
|
2812
|
+
const score = Math.round((maintainability + performance) / 2);
|
|
2813
|
+
const complexity = Math.max(
|
|
2814
|
+
1,
|
|
2815
|
+
(ctx.fileMaxSelectorComplexity ?? 0) + (ctx.fileNestingDepth ?? 0)
|
|
2816
|
+
);
|
|
2817
|
+
return {
|
|
2818
|
+
complexity,
|
|
2819
|
+
score: Math.min(100, Math.max(0, score)),
|
|
2820
|
+
maintainability: Math.min(100, Math.max(0, maintainability)),
|
|
2821
|
+
performance: Math.min(100, Math.max(0, performance)),
|
|
2822
|
+
quality: score >= 80 ? "Excellent" : score >= 60 ? "Good" : "Fair"
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
generateInsights(ctx, frameworks) {
|
|
2826
|
+
const insights = [];
|
|
2827
|
+
let hasTailwind = false;
|
|
2828
|
+
let hasBootstrap = false;
|
|
2829
|
+
let hasCSSModules = false;
|
|
2830
|
+
let hasSCSS = false;
|
|
2831
|
+
for (let i = 0; i < frameworks.length; i++) {
|
|
2832
|
+
if (frameworks[i] === "Tailwind CSS") hasTailwind = true;
|
|
2833
|
+
if (frameworks[i] === "Bootstrap" || frameworks[i] === "Bootstrap / Grid System") hasBootstrap = true;
|
|
2834
|
+
if (frameworks[i] === "CSS Modules") hasCSSModules = true;
|
|
2835
|
+
if (frameworks[i] === "SCSS/SASS") hasSCSS = true;
|
|
2836
|
+
}
|
|
2837
|
+
if (hasTailwind) insights.push("Utilizes Tailwind utility paradigms");
|
|
2838
|
+
if (hasBootstrap) insights.push("Utilizes Bootstrap grid and component paradigms");
|
|
2839
|
+
if (hasCSSModules) insights.push("Utilizes isolated CSS Modules for component styling");
|
|
2840
|
+
if (hasSCSS) insights.push("Leverages SCSS/SASS preprocessor macros and mixins");
|
|
2841
|
+
if (ctx.patterns["Complex Selectors"]) insights.push("Deep nesting/complex specificity detected");
|
|
2842
|
+
if (ctx.patterns["Responsive Design (Media Queries)"]) insights.push("Implements responsive viewports");
|
|
2843
|
+
if (ctx.patterns["CSS Custom Properties"]) insights.push("Leverages native CSS variables for theming");
|
|
2844
|
+
if (ctx.patterns["Cascade Layers (@layer)"]) insights.push("Uses Cascade Layers for architectural isolation");
|
|
2845
|
+
if (ctx.patterns["Container Queries (@container)"]) insights.push("Implements component-driven Container Queries");
|
|
2846
|
+
return insights;
|
|
2847
|
+
}
|
|
2848
|
+
nodeToRange(node) {
|
|
2849
|
+
return {
|
|
2850
|
+
startLine: node.startPosition.row + 1,
|
|
2851
|
+
startColumn: node.startPosition.column + 1,
|
|
2852
|
+
endLine: node.endPosition.row + 1,
|
|
2853
|
+
endColumn: node.endPosition.column + 1,
|
|
2854
|
+
startIndex: node.startIndex,
|
|
2855
|
+
endIndex: node.endIndex
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
|
|
2860
|
+
export {
|
|
2861
|
+
cssFileScopeOf,
|
|
2862
|
+
RealCssAnalyzer
|
|
2863
|
+
};
|