diff --git a/.gitattributes b/.gitattributes index a36bf919..83b87289 100644 --- a/.gitattributes +++ b/.gitattributes @@ -29,3 +29,7 @@ *.jpg binary *.pdf binary *.sqlite binary + +# A torture fixture whose CRLF line endings ARE the trap it sets (a BOM and CRLF in one +# page); normalising it would remove the case the suite asserts. +parser/src/test-data/web/torture/quirks.html -text diff --git a/.github/workflows/build-engines.yml b/.github/workflows/build-engines.yml index bd8b39bf..08bbdfdd 100644 --- a/.github/workflows/build-engines.yml +++ b/.github/workflows/build-engines.yml @@ -73,7 +73,9 @@ jobs: id="$(bash graph/pipeline/run-souffle.sh --language "$lang" --print-engine-id)" echo "$lang: $id"; printf '%s' "$id" > "gen/$lang.id" bash graph/pipeline/run-souffle.sh --language "$lang" --emit-program "gen/$lang.dl" - souffle -I graph -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; } + # -j makes souffle EMIT the parallel loops (without it the C++ has none); whether a + # platform's binary runs them in parallel is decided at compile time below. + souffle -I graph -j 8 -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; } awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "gen/$lang.gen.log" done # the query programs: self-contained (no #include), keyed by dl_program.py itself @@ -89,6 +91,33 @@ jobs: bash .github/scripts/query-smoke.sh expect gen/queries cp .github/scripts/query-smoke.sh gen/queries/smoke.sh cp -r /usr/include/souffle gen/souffle + # the seqlock fix (see souffle_overlay in run-souffle.sh): the write-entry RMW + # must be seq_cst or a weakly-ordered CPU lets data stores pass the version-odd + # store and readers validate garbage. Patched here so every platform's binary + # is built from the same fixed headers; the engine id carries +seqlock-fix-4, + # so these headers must carry the SAME patches run-souffle's overlay applies — + # a binary labeled fix-4 but built from lesser headers is the exact mislabeling + # the salt exists to prevent. + # the header layout differs by install (deb: souffle/utility; brew: souffle/souffle/utility), + # so the patched files are FOUND, not assumed — a miss fails the build here, loudly + PU=$(find gen/souffle -name ParallelUtil.h | head -1); [ -n "$PU" ] + sed -i 's/version\.fetch_or(0x1, std::memory_order_acquire)/version.fetch_or(0x1, std::memory_order_seq_cst)/g' "$PU" + grep -q 'fetch_or(0x1, std::memory_order_seq_cst)' "$PU" + ! grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$PU" + # fix-3: the publication fences, applied by the same anchored patch the local + # overlay uses (extracted from run-souffle.sh so the two cannot drift) + BT=$(find gen/souffle -name BTree.h | head -1); [ -n "$BT" ] + awk '/^import glob, os, sys$/,/^PYEOF$/' graph/pipeline/run-souffle.sh | sed '$d' > /tmp/fences.py + python3 /tmp/fences.py "$BT" + [ "$(grep -c 'seqlock-fix-3' "$BT")" = "4" ] + # fix-4: the speculative-fetch guards, applied by the same extracted patch + FW=$(find gen/souffle -name ConcurrentFlyweight.h | head -1); [ -n "$FW" ] + RT=$(find gen/souffle -name RecordTableImpl.h | head -1); [ -n "$RT" ] + [ "$(grep -c 'seqlock-fix-4' "$FW")" = "1" ] + [ "$(grep -c 'seqlock-fix-4' "$RT")" = "1" ] + [ "$(grep -c 'seqlock-fix-4' "$BT")" = "1" ] + BD=$(find gen/souffle -name BTreeDelete.h | head -1); [ -n "$BD" ] + [ "$(grep -c 'seqlock-fix-4' "$BD")" = "1" ] # one key for the whole set; a partial match restores the previous set echo "key=$(cat gen/*.id gen/queries/*.id | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" # The compile flags live in THIS file and ENGINE_ID does not cover them, so its @@ -135,7 +164,15 @@ jobs: for lang in $LANGUAGES; do if cmp -s "gen/$lang.id" "engines/$lang/ENGINE_ID"; then echo "$lang: cached, rules unchanged"; continue; fi mkdir -p "engines/$lang" - c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang" + # OpenMP on BOTH architectures: the arm64 crash mode was two weak-ordering + # holes in the vendored headers (write-entry RMW + unfenced node publication), + # both patched above and validated 10/10 under load on linux-arm64 and + # darwin-arm64 (seqlock-fix-3). libgomp links STATICALLY so the binary runs on + # machines with no gcc runtime. The .parallel marker beside the binary is what + # run-souffle.sh reads to pass a real -j at run time. + OMP="-fopenmp -Wl,-Bstatic,-lgomp,-Bdynamic" + c++ -std=c++17 -O3 -w $OMP -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang" + [ -n "$OMP" ] && touch "engines/$lang/axiomcode-engine-$lang.parallel" cp "gen/$lang.id" "engines/$lang/ENGINE_ID" done mkdir -p engines/queries @@ -169,8 +206,9 @@ jobs: fc /b gen\%%L.id engines\%%L\ENGINE_ID >nul 2>&1 if errorlevel 1 ( if not exist engines\%%L mkdir engines\%%L - cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /I gen gen\%%L.cpp /Fe:engines\%%L\axiomcode-engine-%%L.exe + cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /openmp /I gen gen\%%L.cpp /Fe:engines\%%L\axiomcode-engine-%%L.exe if errorlevel 1 exit /b 1 + type nul > engines\%%L\axiomcode-engine-%%L.exe.parallel copy /y gen\%%L.id engines\%%L\ENGINE_ID ) else ( echo %%L: cached, rules unchanged diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb6710f5..a9920765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ permissions: contents: read env: - # bin/axiomcode and the parser need Node ≥ 22.5. + # bin/axiomcode and the parser need Node ≥ 22.13 (node:sqlite without a flag). NODE_VERSION: '22' SOUFFLE_VERSION: '2.5' SOUFFLE_SHA512: '6b86e554f6aa5abf8a8b55d8312ae37c0957c5bd6c9edeea89246db9406f645ec5e600b84fe6636b1c163da556f0da6c3d2dad46c1083413f2fcf4f95b9ac62c' @@ -371,6 +371,16 @@ jobs: if: matrix.lang == 'csharp' run: dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle + # `--library auto` decompiles a dependency that ships no sources jar; the Java query case for it needs Vineflower + - name: fetch the decompiler `--library auto` uses for Java class jars + if: matrix.lang == 'java' + run: mvn -q dependency:get -Dartifact=org.vineflower:vineflower:1.10.1 + + # `--library auto` compiles a NuGet package by decompiling it; the query case for it needs the decompiler + - name: install the decompiler `--library auto` uses for NuGet packages + if: matrix.lang == 'csharp' + run: dotnet tool install -g ilspycmd --version 8.2.0.7535 + - name: cache the Soufflé package uses: actions/cache@v4 with: diff --git a/README.md b/README.md index 166aaf3d..c388a185 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm Nightly (dev) License: FSL-1.1-Apache-2.0 - Node ≥ 22.5 + Node ≥ 22.13

@@ -125,7 +125,7 @@ AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) pa graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the agent-facing frontend: a skill, four MCP tools, and hooks. Install the engine first. -Requirements: **Node ≥ 22.5** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also +Requirements: **Node ≥ 22.13** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also [Git for Windows](https://git-scm.com/download/win): the CLI runs under its bash. The engine ships as a prebuilt binary for macOS (Apple Silicon and Intel), Linux (x64 and arm64) and Windows x64, and `npm install` takes the one for your platform. No Soufflé and no compiler are needed, with one exception: @@ -255,8 +255,8 @@ looked up again in the graph before you see it; the `verified:` line is that che ### Support for agents -Every agent below gets the four MCP tools and the skill; the hooks, which add the graph's edges to the agent's -own file reads and searches, run where the last column says so. +Every agent below gets the four MCP tools and the skill; the hooks, which keep the graph current and report what an +edit breaks, run where the last column says so. No hook annotates the agent's own reads and searches. | Agent | Install | Uninstall | Hooks | |---|---|---|---| @@ -308,12 +308,12 @@ about one change had to read 95 of 43,793 methods, and every true direct caller ## CLI commands -Four questions, each answered as numbered places with the code of the function each one sits in. The MCP server -offers the same four as tools: `find(question)`, `impact(name)`, `path(start, end)` and `tests()`. +Three questions, each answered as numbered places with the code of the function each one sits in. The MCP server +offers the same three as tools: `impact(name)`, `path(start, end)` and `tests()`. Finding where code lives is +left to your own search: bring the name you found to these commands. | command | what it answers | |---|---| -| `axiomcode find ""` | where the code for a task lives, when you have it in words and not yet a name | | `axiomcode impact ` | who calls it, what a change to it reaches, and the tests that exercise it | | `axiomcode impact` | the same for the declarations your uncommitted edits changed; the answer starts with `your edits:` | | `axiomcode path ` | how A reaches B: every hop of the call chain, with the code at each call | @@ -334,9 +334,9 @@ command's usage. The graph stays current on its own. Every file the parser reads is recorded with its hash at build time; after an edit, a shell command, a finished turn, at session start, and before a query, anything that differs starts one background rebuild per repository, with the language, `--src` and `--library` of the graph it replaces. Every -command keeps reading the previous graph until the new one is indexed and swapped in. A query waits up to -`AXIOMCODE_FRESH_WAIT` seconds (default 10) for it, then answers from the previous graph with a `graph refresh:` line -naming the files it predates. The MCP server also checks every repository it has answered for once 15 minutes have +command keeps reading the previous graph until the new one is indexed and swapped in. A query answers from the +previous graph at once, with a `graph refresh:` line naming the files it predates and the rows in them marked; +`--fresh` waits for the rebuild instead, and `AXIOMCODE_FRESH_WAIT` (seconds, default 0) lets every query wait that long. The MCP server also checks every repository it has answered for once 15 minutes have passed since its last update (`AXIOMCODE_REFRESH_INTERVAL`, seconds; 0 turns it off), which catches edits made while a session sits idle. The graph records when and why it was built in `index_meta` (`refreshed_at`, `refresh_reason`). `AXIOMCODE_NO_REFRESH=1` turns the rebuilds off, not the check: an answer from a graph older than an edit still diff --git a/bin/axiomcode b/bin/axiomcode index 6b902cf0..99f3f668 100755 --- a/bin/axiomcode +++ b/bin/axiomcode @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # axiomcode — ask a repository's call graph. `axiomcode --help` prints the dispatcher's help -# (plugins/axiomcode/skills/axiomcode/scripts/axiomcode): index, find, impact, path and tests. +# (plugins/axiomcode/skills/axiomcode/scripts/axiomcode): index, context, impact, path and tests. # ───────────────────────────────────────────────────────────────────────────── # INTERNAL COMMANDS, not advertised: the build, the engine suites and the MCP server, which # the dispatcher, the test suites and the agent manifests call. @@ -98,16 +98,17 @@ PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}" QUERY="$ROOT/plugins/axiomcode/skills/axiomcode/scripts/axiomcode" # `tests` is a query verb (the tests an edit reaches); `test` is THIS command's engine suite. The query # surface advertises `tests`; `test` is internal. -# The list is read from the frontend's own dispatch table, the lines ` [|]) exec …` that its `--verbs` -# prints, so the two entry points still cannot drift -- but read here, in this shell, instead of by a second bash, a -# sed, a tr and two greps on every query. +# THE LIST IS THE SUPPORTED SURFACE: the `# axiomcode ` lines of the frontend's own leading help (what its +# `--verbs` prints), so the two entry points cannot drift -- read here, in this shell, instead of by a second bash. +# The frontend still dispatches internal verbs (context, changed, test-impact) for the hooks and the suites, which +# call it directly; this command refuses them, so the installed surface is only what the help teaches. QVERBS=() load_verbs(){ [ ${#QVERBS[@]} -gt 0 ] && return 0; [ -f "$QUERY" ] || return 0 - local l v x re='^ ([a-z|-]*)\) *exec ' + local l re='^# axiomcode ([a-z][a-z-]*)' while IFS= read -r l || [ -n "$l" ]; do - [[ $l =~ $re ]] || continue; v="${BASH_REMATCH[1]}" - for x in ${v//|/ }; do QVERBS+=("$x"); done + [ "${l:0:1}" = '#' ] || { [ -n "${QVERBS[*]:-}" ] && break; continue; } + [[ $l =~ $re ]] && QVERBS+=("${BASH_REMATCH[1]}") done < "$QUERY" } query_verbs(){ load_verbs; [ ${#QVERBS[@]} -gt 0 ] && printf '%s\n' "${QVERBS[@]}"; return 0; } @@ -294,6 +295,13 @@ case "$cmd" in echo "▶ parsing $src → IR" "$0" parser "$src" "$ir" ${popts[@]+"${popts[@]}"} > "$int/parse.log" 2>&1 || { echo "❌ parser failed — see $int/parse.log" >&2; tail -5 "$int/parse.log" >&2; exit 1; } head -1 "$int/parse.log" + # WHAT AN INSTALLED PYTHON LIBRARY HANDS BACK to the objects the client gives it (graph/python/libsum): read from + # the project's environment, cached by package and version, staged beside the client IR. Never fatal -- without + # it the engine sees no library hand-backs, exactly as before. AXIOMCODE_LIB_CALLBACKS=0 turns it off. + if [ -d "$ir/python" ] && [ "${AXIOMCODE_LIB_CALLBACKS:-1}" != 0 ]; then + python3 "$ROOT/graph/python/libsum/libsum.py" --src "$src" --out "$ir/python" > "$int/libsum.log" 2>&1 \ + && tail -1 "$int/libsum.log" || echo "⚠️ library callback summaries failed (see $int/libsum.log); continuing without them" >&2 + fi # which languages the IR holds — one folder each — and which of them have an engine found=(); for d in "$ir"/*/; do l="$(basename "$d")"; [ -d "$ROOT/graph/$l/engine" ] && found+=("$l"); done [ ${#found[@]} -gt 0 ] || { echo "❌ no language with an engine in the IR (folders: $(ls "$ir" | tr '\n' ' '))" >&2; exit 1; } @@ -324,16 +332,61 @@ case "$cmd" in # a caller that checks it is no worse off. Only the languages that produced a graph are # listed at the end, so the output stays a list of files that exist. solved=(); failed=() - for l in "${langs[@]}"; do - echo "▶ solving $l → $out/$l/graph.sqlite" - if "$0" engine --language "$l" --client-ir "$ir/$l" --out "$out/$l" --intermediate "$int" --library "$libroots" \ - --meta "source_version=$version" --meta "source_dir=$(cd "$src" && pwd)" ${rest[@]+"${rest[@]}"}; then - solved+=("$l"); [ -z "$progress" ] || echo "$l ok $out/$l/graph.sqlite" >> "$progress" + # Each language gets its OWN intermediate: run-souffle writes fixed names there + # (souffle-program.cpp, .souffle-gen.log), so two concurrent solves into one + # directory would overwrite each other's program mid-compile. + solve_one(){ local sl="$1" + "$0" engine --language "$sl" --client-ir "$ir/$sl" --out "$out/$sl" --intermediate "$int/$sl" --library "$libroots" \ + --meta "source_version=$version" --meta "source_dir=$(cd "$src" && pwd)" ${rest[@]+"${rest[@]}"} + } + record(){ local rl="$1" rrc="$2" + if [ "$rrc" -eq 0 ]; then + solved+=("$rl"); [ -z "$progress" ] || echo "$rl ok $out/$rl/graph.sqlite" >> "$progress" else - failed+=("$l"); echo "❌ $l failed; continuing with the remaining languages" >&2 - [ -z "$progress" ] || echo "$l failed" >> "$progress" + failed+=("$rl"); echo "❌ $rl failed; continuing with the remaining languages" >&2 + [ -z "$progress" ] || echo "$rl failed" >> "$progress" fi - done + } + # THE FIRST (LARGEST) LANGUAGE SOLVES ALONE, streaming, exactly as before: its graph is the one a + # --progress caller publishes first (#1555), and nothing may compete with it for cores or + # interleave its log. Only the languages AFTER it run concurrently — they used to wait in line + # behind each other for no reason: their solves share nothing (own IR, own output, own + # intermediate). Memory is the tradeoff (each is its own Soufflé process), so the width is + # modest by default and AXIOMCODE_SOLVE_JOBS raises or lowers it; 1 restores the strict line. + l="${langs[0]}" + echo "▶ solving $l → $out/$l/graph.sqlite" + rc=0; solve_one "$l" || rc=$?; record "$l" "$rc" + if [ ${#langs[@]} -gt 1 ]; then + others=("${langs[@]:1}") + JOBS="${AXIOMCODE_SOLVE_JOBS:-2}" + case "$JOBS" in (*[!0-9]*|'') JOBS=2;; esac; [ "$JOBS" -ge 1 ] || JOBS=1 + if [ "$JOBS" -eq 1 ]; then + for l in "${others[@]}"; do + echo "▶ solving $l → $out/$l/graph.sqlite" + rc=0; solve_one "$l" || rc=$?; record "$l" "$rc" + done + else + # Waves of $JOBS (bash 3.2 has no `wait -n`): start a wave, wait for all of it, replay each + # log whole so the build log never interleaves, then record in the wave's order. + i=0 + while [ $i -lt ${#others[@]} ]; do + wave=("${others[@]:$i:$JOBS}"); i=$((i + JOBS)) + wavepids=() + for l in "${wave[@]}"; do + echo "▶ solving $l → $out/$l/graph.sqlite (concurrent)" + { solve_one "$l" > "$int/solve-$l.log" 2>&1; echo $? > "$int/solve-$l.rc"; } & + wavepids+=($!) + done + for wp in "${wavepids[@]}"; do wait "$wp" || true; done + for l in "${wave[@]}"; do + cat "$int/solve-$l.log" 2>/dev/null + rc="$(cat "$int/solve-$l.rc" 2>/dev/null || echo 1)" + rm -f "$int/solve-$l.log" "$int/solve-$l.rc" + record "$l" "$rc" + done + done + fi + fi for l in ${solved[@]+"${solved[@]}"}; do echo "$out/$l/graph.sqlite"; done if [ ${#failed[@]} -gt 0 ]; then echo "❌ ${#failed[@]} of ${#langs[@]} languages failed: ${failed[*]}" >&2 @@ -347,7 +400,7 @@ case "$cmd" in run_suite(){ local s="$1"; shift; echo "══ $s"; bash "$ROOT/graph/test/$s/run-tests.sh" "$@" || rc=1; } case "$which" in java|typescript|python|javascript|csharp) run_suite "$which" "$@";; - parser) for t in java-extractor-tests typescript-tests python-tests javascript-tests csharp-tests gradle-tests services-tests discovery-tests; do + parser) for t in java-extractor-tests typescript-tests python-tests javascript-tests csharp-tests gradle-tests services-tests web-tests web-torture discovery-tests; do [ -f "$ROOT/parser/src/test/$t.ts" ] || continue; echo "══ parser: $t"; (cd "$ROOT/parser" && npx tsx "src/test/$t.ts") || rc=1; done;; all) for l in java typescript python javascript csharp; do run_suite "$l" "$@"; done; "$0" test parser || rc=1;; *) die "test: unknown suite $which (java | typescript | python | javascript | csharp | parser | all)";; diff --git a/bin/axiomcode.js b/bin/axiomcode.js index 72a1fc1a..3f54b1e5 100755 --- a/bin/axiomcode.js +++ b/bin/axiomcode.js @@ -37,6 +37,14 @@ if (args.length === 1 && args[0] === '--version') { process.exit(0); } +// THE NODE FLOOR, SAID BEFORE ANYTHING RUNS. The graph is written with node:sqlite, which Node ships without a flag from +// 22.13 (and 23.4); on 22.5-22.12 the index failed minutes in with "No such built-in module: node:sqlite". Asked by +// version, not by loading the module, which would print Node's experimental-feature warning on every command. +const [nodeMaj, nodeMin] = process.versions.node.split('.').map(Number); +if (!(nodeMaj > 23 || (nodeMaj === 23 && nodeMin >= 4) || (nodeMaj === 22 && nodeMin >= 13))) { + fail(`axiomcode needs Node 22.13 or newer (it writes the graph with node:sqlite); this is Node ${process.versions.node}.`); +} + const { bash, error } = findBash(); if (error) fail(error); const py = findPython(); diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 2c17b391..a3f36c76 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -442,7 +442,7 @@ One row per place a call is written (or, for a synthesised edge, the construct t - **typescript** — end_line / end_column come from the expression row; the call-site row itself records only the start. - **javascript** — caller_id is the parser's enclosing method, or the module initializer for top-level code. end_line / end_column come from the expression row. `require()` is a module edge, not a call site. - **typescript** — PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor's name is on the callee's methods row. Filter them out with kind NOT IN (…) when counting calls. -- **python** — PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. +- **python** — PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. - **python** — The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class's TYPE hash for METACLASS_CREATION, positioned at the class declaration. ### `call_edges` @@ -467,6 +467,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `new` | csharp | An object creation. The target is the constructed type's constructor, and it is never dispatched. | | `ctor_delegate` | csharp | `: this(...)` or `: base(...)`. No name is written, so the target is structural. | | `primary_ctor_base` | csharp | SYNTHESISED. A primary constructor's base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference. | +| `implicit_base_ctor` | csharp | SYNTHESISED. The base class's parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site. | | `delegate` | csharp | A call through a delegate value: `handler(x)` or `handler.Invoke(x)`. | | `operator` | csharp | A user-defined operator invoked by operator syntax. | | `conversion` | csharp | A user-defined conversion. An implicit one has no syntax at the call site. | @@ -531,9 +532,13 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `DECORATOR_*` | python | Applying an unparenthesised decorator; the suffix is the parser's decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash. | | `METACLASS_CREATION` | python | A class statement invokes its metaclass's `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base's `__init_subclass__`. No written call; the site is the class's type hash. | | `PROPERTY_READ` | python | Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression. | +| `PROPERTY_WRITE` | python | Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression. | | `CONTEXT_MANAGER` | python | `with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression. | | `ITERATION_PROTOCOL` | python | `for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression. | | `SUBSCRIPT_PROTOCOL` | python | `x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver's class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call. | +| `BUILTIN_PROTOCOL` | python | `repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument's class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call. | +| `OPERATOR_PROTOCOL` | python | An operator runs a dunder of its operands: `a + b` the left operand's `__add__` and the right one's `__radd__`, a comparison the left slot and the right one's mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand's `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression. | +| `TRUTH_PROTOCOL` | python | A truth test runs the tested value's `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression. | **`call_edges.tier` values** diff --git a/graph/bundle/build.ts b/graph/bundle/build.ts index 072e9594..84c9fcb9 100644 --- a/graph/bundle/build.ts +++ b/graph/bundle/build.ts @@ -529,6 +529,21 @@ export async function buildCore(inp: BuildInputs): Promise { } } } + // a field access whose site is no expression: a module variable read in a type (`typeof X`) + if (A.ir.fieldSites && fieldSites.size > 0) { + const L = A.ir.fieldSites; + const src = await clientSource(inp.clientIrDir, L.file); + if (src) { + const h = src.header; + const ci = h.col(L.id), cl = h.col(L.startLine), cel = h.col(L.endLine); + for await (const r of rowsOf(src)) { + const fa = fieldSites.get(r[ci] ?? ''); + if (!fa) continue; + const file = fileOf(L.fileVia, h, r); + for (const fr of fa) { fill(fr, 6, file); fill(fr, 7, int(r[cl])); fill(fr, 9, int(r[cel])); } + } + } + } // a site the IR writes no name for, where the engine derived the accessor it calls (#1441) if (A.raw.siteNames) { for (const [site, name] of await readSource(rawDir, A.raw.siteNames)) { diff --git a/graph/bundle/cli.ts b/graph/bundle/cli.ts index adb2061c..ad975fad 100644 --- a/graph/bundle/cli.ts +++ b/graph/bundle/cli.ts @@ -95,7 +95,7 @@ export async function main(argv = process.argv.slice(2)): Promise { log(`▶ wrote ${dbPath}${a.debug ? ' (+ csv/*.csv, --debug)' : ''}`); } else { fs.rmSync(dbPath, { force: true }); - console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.5) — graph.sqlite NOT written; csv/*.csv is complete`); + console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.13) — graph.sqlite NOT written; csv/*.csv is complete`); } log(`▶ bundle complete in ${((Date.now() - t0) / 1000).toFixed(1)}s`); } diff --git a/graph/bundle/languages.ts b/graph/bundle/languages.ts index 286ec18a..e4ec00d9 100644 --- a/graph/bundle/languages.ts +++ b/graph/bundle/languages.ts @@ -166,6 +166,8 @@ export interface LanguageAdapter { callSites?: CallSitesIR; decorators?: DecoratorsIR; localSites?: LocalSitesIR; + /** field access sites that are not expressions (TypeScript: a `typeof X` type reference), positioned from this table */ + fieldSites?: LocalSitesIR; /** absent where the front end writes no skipped-files report */ skipped?: SkippedIR; }; @@ -277,6 +279,11 @@ const TYPESCRIPT: LanguageAdapter = { startLine: 'startLine', startColumn: 'startColumn', fileVia: { column: 'tsModuleLinkHash', through: 'modules' }, }, + // a module variable read in a TYPE (`typeof X`): the field_access site is the type reference + fieldSites: { + file: 'all-typescript-type-references.csv', id: 'tsTypeReferenceUniqueHash', startLine: 'startLine', endLine: 'endLine', + fileVia: { column: 'tsModuleLinkHash', through: 'modules' }, + }, skipped: { file: 'skipped-typescript-files.csv', filePath: 'filePath', reason: 'reason', detail: 'detail' }, }, }; @@ -337,6 +344,9 @@ const JAVASCRIPT: LanguageAdapter = { typeAncestors: { file: 'resolution-type-ancestor.csv', columns: [0, 1] }, entryPoints: { file: 'entry-point.csv', columns: [0, 1] }, entryReachable: { file: 'entry-reachable.csv', columns: [0] }, + // site, caller, variable, provenance, tier, access: a module variable an identifier reads, + // directly or through an import binding (the field is the variable's own hash) + fieldAccess: { file: 'field-access.csv', columns: [0, 1, 2, 3, 4, 5] }, }, ir: { // No signature and no owner qualified name: JavaScript declares neither. diff --git a/graph/bundle/node-sqlite.d.ts b/graph/bundle/node-sqlite.d.ts index a2d35d8c..618db642 100644 --- a/graph/bundle/node-sqlite.d.ts +++ b/graph/bundle/node-sqlite.d.ts @@ -1,5 +1,5 @@ /** - * The slice of `node:sqlite` this package uses. Node ships the module from 22.5; the + * The slice of `node:sqlite` this package uses. Node ships the module unflagged from 22.13; the * installed @types/node predates it, so the four members used here are declared locally * rather than pulling a newer type package for one import. */ diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 2e9af322..3ff97670 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -358,6 +358,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'new', languages: C, meaning: 'An object creation. The target is the constructed type\'s constructor, and it is never dispatched.' }, { table: 'call_edges', column: 'kind', value: 'ctor_delegate', languages: C, meaning: '`: this(...)` or `: base(...)`. No name is written, so the target is structural.' }, { table: 'call_edges', column: 'kind', value: 'primary_ctor_base', languages: C, meaning: 'SYNTHESISED. A primary constructor\'s base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference.' }, + { table: 'call_edges', column: 'kind', value: 'implicit_base_ctor', languages: C, meaning: 'SYNTHESISED. The base class\'s parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site.' }, { table: 'call_edges', column: 'kind', value: 'delegate', languages: C, meaning: 'A call through a delegate value: `handler(x)` or `handler.Invoke(x)`.' }, { table: 'call_edges', column: 'kind', value: 'operator', languages: C, meaning: 'A user-defined operator invoked by operator syntax.' }, { table: 'call_edges', column: 'kind', value: 'conversion', languages: C, meaning: 'A user-defined conversion. An implicit one has no syntax at the call site.' }, @@ -667,9 +668,13 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'DECORATOR_*', languages: P, meaning: 'Applying an unparenthesised decorator; the suffix is the parser\'s decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash.' }, { table: 'call_edges', column: 'kind', value: 'METACLASS_CREATION', languages: P, meaning: 'A class statement invokes its metaclass\'s `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base\'s `__init_subclass__`. No written call; the site is the class\'s type hash.' }, { table: 'call_edges', column: 'kind', value: 'PROPERTY_READ', languages: P, meaning: 'Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression.' }, + { table: 'call_edges', column: 'kind', value: 'PROPERTY_WRITE', languages: P, meaning: 'Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression.' }, { table: 'call_edges', column: 'kind', value: 'CONTEXT_MANAGER', languages: P, meaning: '`with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression.' }, { table: 'call_edges', column: 'kind', value: 'ITERATION_PROTOCOL', languages: P, meaning: '`for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression.' }, { table: 'call_edges', column: 'kind', value: 'SUBSCRIPT_PROTOCOL', languages: P, meaning: '`x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver\'s class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call.' }, + { table: 'call_edges', column: 'kind', value: 'BUILTIN_PROTOCOL', languages: P, meaning: '`repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument\'s class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call.' }, + { table: 'call_edges', column: 'kind', value: 'OPERATOR_PROTOCOL', languages: P, meaning: 'An operator runs a dunder of its operands: `a + b` the left operand\'s `__add__` and the right one\'s `__radd__`, a comparison the left slot and the right one\'s mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand\'s `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression.' }, + { table: 'call_edges', column: 'kind', value: 'TRUTH_PROTOCOL', languages: P, meaning: 'A truth test runs the tested value\'s `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression.' }, // entry_points.reason { table: 'entry_points', column: 'reason', value: 'main', languages: ['java', 'csharp'], meaning: 'A static `main`. C#: a static `Main`, or the method top-level statements compile to.' }, @@ -748,7 +753,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'typescript', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`.' }, { language: 'typescript', table: 'type_instantiated', note: 'Every row has how = `new`. Not restricted to client provenance: a type the library constructs is still a type that exists at run time, and dropping it would narrow the envelope unsoundly.' }, { language: 'typescript', table: 'call_sites', note: 'PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor\'s name is on the callee\'s methods row. Filter them out with kind NOT IN (…) when counting calls.' }, - { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, + { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, { language: 'python', table: 'call_sites', note: 'The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class\'s TYPE hash for METACLASS_CREATION, positioned at the class declaration.' }, { language: 'python', table: 'call_edges', note: 'A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row.' }, { language: 'java', table: 'call_edges', note: 'A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform\'s own subclasses, are the other possible targets. Stage the library to replace the label with the real method.' }, diff --git a/graph/bundle/write.ts b/graph/bundle/write.ts index 77724016..d00899fe 100644 --- a/graph/bundle/write.ts +++ b/graph/bundle/write.ts @@ -20,10 +20,11 @@ export function writeCoreCsv(graphDir: string, core: CoreTables, log: (s: string } } -/** Node ≥ 22.5 ships `node:sqlite`. */ +/** Whether `node:sqlite` loads here. Asked of the module, not the version: Node 22.5–22.12 and 23.0–23.3 ship it only + * behind --experimental-sqlite, and a version test passed them, so the import that followed failed with "No such + * built-in module: node:sqlite" on Node 22.11. */ export function sqliteAvailable(): boolean { - const [maj, min] = process.versions.node.split('.').map(Number); - return maj! > 22 || (maj === 22 && min! >= 5); + try { require('node:sqlite'); return true; } catch { return false; } } function createSql(t: TableSpec, prefix = ''): string { @@ -42,6 +43,35 @@ export interface SqliteInputs { log: (s: string) => void; } +// One INSERT per row pays the statement dispatch and argument binding once per row — half the +// whole bundle stage on a 1.4GB graph (63.6s of 127s profiled, 4,461-file subject). A multi-row +// VALUES list amortizes both; 128 rows per statement keeps the flattened argument array well +// under sqlite's bind limit at the widest table. +const BATCH = Math.max(1, Number(process.env.AXIOM_BUNDLE_BATCH ?? 128) || 128); +type DbLike = { prepare(sql: string): { run(...a: (string | number | null)[]): unknown } }; +function batchInsert(db: DbLike, table: string, arity: number, verb: string): { push(r: (string | number | null)[]): void; flush(): void } { + const one = `(${Array.from({ length: arity }, () => '?').join(', ')})`; + if (BATCH === 1) { + // the pre-batching path, byte for byte: one prepared statement, one run per row + const single = db.prepare(`${verb} INTO ${table} VALUES ${one}`); + return { push(r) { single.run(...r.slice(0, arity).map((v) => v ?? null)); }, flush() {} }; + } + const full = db.prepare(`${verb} INTO ${table} VALUES ${Array.from({ length: BATCH }, () => one).join(', ')}`); + let buf: (string | number | null)[] = []; + let n = 0; + return { + push(r) { + for (let i = 0; i < arity; i++) buf.push(r[i] ?? null); + if (++n === BATCH) { full.run(...buf); buf = []; n = 0; } + }, + flush() { + if (n === 0) return; + const rest = db.prepare(`${verb} INTO ${table} VALUES ${Array.from({ length: n }, () => one).join(', ')}`); + rest.run(...buf); buf = []; n = 0; + }, + }; +} + export async function writeSqlite(inp: SqliteInputs): Promise { // Loaded lazily so the CSV path works on a Node without the module. const { DatabaseSync } = await import('node:sqlite'); @@ -55,8 +85,9 @@ export async function writeSqlite(inp: SqliteInputs): Promise { for (const t of CORE_TABLES) { db.exec(createSql(t)); const rows = (inp.core as unknown as Record)[t.name] ?? []; - const ins = db.prepare(`INSERT OR IGNORE INTO ${t.name} VALUES (${t.columns.map(() => '?').join(', ')})`); - for (const r of rows) ins.run(...(r as (string | number | null)[])); + const ins = batchInsert(db, t.name, t.columns.length, 'INSERT OR IGNORE'); + for (const r of rows) ins.push(r as (string | number | null)[]); + ins.flush(); inp.log(` sqlite ${t.name}: ${rows.length} rows`); } db.exec('COMMIT;'); @@ -68,12 +99,13 @@ export async function writeSqlite(inp: SqliteInputs): Promise { const name = `ext_${e.relation}`; db.exec(`CREATE TABLE ${name} (${cols.join(', ')});`); if (e.arity === 0) continue; - const ins = db.prepare(`INSERT INTO ${name} VALUES (${cols.map(() => '?').join(', ')})`); + const ins = batchInsert(db, name, e.arity, 'INSERT'); let n = 0; for await (const r of readRaw(path.join(inp.rawDir, e.file))) { if (r.length !== e.arity) continue; // a torn row is dropped, as the engine's own staging does - ins.run(...r); n++; + ins.push(r); n++; } + ins.flush(); if (n > 0) inp.log(` sqlite ${name}: ${n} rows`); } db.exec('COMMIT;'); diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl index b44340cf..c0e3de06 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -286,6 +286,52 @@ call_chain_edge(tr, caller, "-", ctor, "lib", "boundary_lib", "primary_ctor_base method_decl("lib", _, _, _, ctor), type_ctor("client", gk, caller). +// ── SYNTHESISED EDGES: THE IMPLICIT `base()` ──────────────────────────────── +// A constructor that writes neither `: base(..)` nor `: this(..)` runs its base +// class's parameterless constructor before its own body, and a class that declares +// no constructor at all gets one that does the same. Neither is written anywhere, so +// without these edges a base constructor that every derived class runs has no caller. +// +// implicit_base_ctor(TypeGroup, Ctor): the constructor `base()` runs for a type -- +// its base class's constructor that takes no argument, or, where the base declares +// no constructor either, the one the base's own implicit constructor runs. +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + type_ctor("client", bgk, ctor), ctor_accepts_argc("client", ctor, "0"). +implicit_base_ctor(gk, ctor) :- + type_extends("client", gk, bgk), + !type_ctor("client", bgk, _), type_group("client", _, bgk), + implicit_base_ctor(bgk, ctor). + +// A constructor that hands off with `: this(..)` or `: base(..)`, or a primary +// constructor whose heritage passes arguments: the base constructor it runs is the +// one written there, already an edge. +ctor_has_initializer(m) :- call_is_base_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- call_is_this_ctor("client", e), expr_ultimate_method("client", e, m). +ctor_has_initializer(m) :- + heritage_has_ctor_args("client", t, _), type_group("client", t, gk), type_ctor("client", gk, m). + +// The FromExpr is the base class's entry in the heritage clause, as for a primary +// constructor's base invocation: the closest thing to where the call is written. +implicit_base_site(gk, tr) :- + type_extends("client", gk, bgk), + heritage_resolves("client", gk, pos, bgk), + heritage_of_entity("client", gk, pos, _, _, tr), tr != "". + +// (1) A declared constructor with no initializer. +call_chain_edge(tr, m, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + type_ctor("client", gk, m), method_decl("client", _, _, _, m), + !ctor_has_initializer(m), + implicit_base_ctor(gk, ctor), + implicit_base_site(gk, tr). + +// (2) `new T()` where T declares no constructor: the implicit one runs T's base's. +// The edge sits on the `new` site itself, beside its known_implicit_ctor row. +call_chain_edge(e, caller, "-", ctor, "client", "known_edge", "implicit_base_ctor") :- + call_ctor_implicit("client", e, gk), + implicit_base_ctor(gk, ctor), + call_from_expr("client", e, caller). + // ── ACCESSOR EDGES: A PROPERTY READ IS A CALL ─────────────────────────────── // `x.Name` invokes get_Name, `a[i]` an indexer accessor, `e += h` an add accessor. // These have no cs_call_site row -- the parser cannot know whether `x.Name` is a @@ -485,6 +531,10 @@ call_site_dropped(e) :- csite("client", e), !site_in_output(e). // call_chain_summary(Tier, Count) -- the per-tier tally, which is the one number a // run is judged on. +// One count per DISTINCT tier: driven by call_class rows, the aggregate re-counted the +// tier once per SITE in it (sites x tiers — 9.8s of a 36s solve for 7 rows, the single +// hottest rule of the C# profile). Same fix as the python engine's site_class_kind. +call_class_kind(cls) :- call_class("client", _, cls). call_chain_summary(cls, n) :- - call_class("client", _, cls), + call_class_kind(cls), n = count : { call_class("client", _, cls) }. diff --git a/graph/csharp/engine/containment/type-nesting.dl b/graph/csharp/engine/containment/type-nesting.dl index e584db41..8ac8b94d 100644 --- a/graph/csharp/engine/containment/type-nesting.dl +++ b/graph/csharp/engine/containment/type-nesting.dl @@ -149,3 +149,27 @@ module_in_scope(prov, mod, outer) :- // The GLOBAL namespace ("") is always in scope. module_in_scope(prov, mod, "") :- module_decl(prov, _, _, _, mod). + +// module_ns_chain(Prov, ModuleHash, Namespace) -- the namespaces a name written in this +// file is read from: the file's own namespaces and every dotted prefix of them, as +// text. A prefix that holds no type of its own (`Acme` over `Acme.Shop`) still +// qualifies a name written below it, so this is not ns_encloses, which needs one. +module_ns_chain(prov, mod, ns) :- module_namespace_own(prov, mod, ns), ns != "". +module_ns_chain(prov, mod, p) :- module_namespace_own(prov, mod, inner), ns_prefix(prov, inner, p). + +// ── A USING WRITTEN INSIDE A NAMESPACE ───────────────────────────────────── +// `namespace Shop.Tests; using Results;` imports Shop.Results. C# reads the name in +// a using directive from the namespace the directive sits in, innermost outward, +// exactly as it reads any other name there. Taken as written it names a namespace +// that does not exist, and every type the file names through it reads as a library +// type. +// +// The IR does not record whether a using sits inside the namespace or above it. +// The language decides it instead: a using whose name is no namespace at all only +// compiles inside a namespace that has it as a child. So the relative reading is +// taken only when the name as written names nothing, and only for a child that +// exists -- a file whose namespaces have no such child gains nothing. +module_in_scope(prov, mod, full) :- + using_namespace(prov, mod, n), !ns_exists(prov, n), + module_ns_chain(prov, mod, e), full = cat(e, cat(".", n)), + ns_exists(prov, full). diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 5c962379..fe5e35e7 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -459,6 +459,25 @@ expr_type(prov, e, gk) :- expr_type(prov, q, qgk), member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). +// A STATIC member read through its TYPE NAME: `Context.Current.Factory.Create()`. +// The qualifier `Context` names a type, not a value, so it has no expr_type and the +// two clauses above cannot start from it: the getter of `Current` is an edge +// (properties.dl reads the type name), and these give its RESULT a type, so that +// `.Factory` and every call after it resolve. ref_names_type already refuses a name +// that also binds a value, which is the language's member-over-type rule. +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "property", p), property_type(prov, p, gk). +expr_type(prov, e, gk) :- + expr_kind(prov, e, "MEMBER_ACCESS"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, qgk), + member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk). + // ── A GENERIC ARGUMENT, SUBSTITUTED FOR THE PARAMETER IT FILLS ────────────── // `IWrap w; w.Value.Flag` -- `Value` is declared `T`, and without binding // T to Settings the access resolves and the type of the RESULT is the type parameter, diff --git a/graph/csharp/engine/projections/expressions.dl b/graph/csharp/engine/projections/expressions.dl index 46e28c76..8a82e1ba 100644 --- a/graph/csharp/engine/projections/expressions.dl +++ b/graph/csharp/engine/projections/expressions.dl @@ -117,6 +117,15 @@ expr_written_name("lib", e, n) :- lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, n, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), n != "". +// ── expr_type_argc(Prov, ExprHash, TypeArgumentCount) ────────────────────── +// The type arguments written on a name: `Cache` is the name `Cache` at arity 1. +// A generic name used as a receiver or qualifier is resolved at this arity, since +// `Cache` and `Cache` are two types. +expr_type_argc("client", e, tac) :- + cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). +expr_type_argc("lib", e, tac) :- + lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e). + // ── expr_literal(Prov, ExprHash, LiteralKind, LiteralValue) ───────────────── expr_literal("client", e, lk, lv) :- cs_expression(_, _, _, _, _, _, _, _, _, _, lk, lv, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), diff --git a/graph/csharp/engine/projections/variables.dl b/graph/csharp/engine/projections/variables.dl index 27be6bf5..841c1f86 100644 --- a/graph/csharp/engine/projections/variables.dl +++ b/graph/csharp/engine/projections/variables.dl @@ -61,6 +61,10 @@ var_initializer("lib", v, e) :- // The initializer IS a value of the variable's type. var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "LOCAL", _, v). var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "CONST", _, v). +// A `using` resource -- `using (var w = new Writer())` and `using var w = ...;` -- is +// the disposable itself, so its initializer is a value of its type exactly as a +// local's is. Without it every call on such a resource has an untyped receiver. +var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "USING", _, v). // The initializer is a COLLECTION and the variable takes its element type. var_iterated_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "FOREACH", _, v). diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl index 23516414..5a09820c 100644 --- a/graph/csharp/engine/resolution/extensions.dl +++ b/graph/csharp/engine/resolution/extensions.dl @@ -118,6 +118,25 @@ call_recv_type_name(prov, e, tn) :- call_recv_type_name(prov, e, tn) :- call_receiver_expr(prov, e, r), ref_denotes(prov, r, "LAMBDA_PARAMETER", p), param_type_name(prov, p, tn, _). +// AN IMPLICITLY TYPED LAMBDA PARAMETER takes its type from the delegate it is +// converted to (lambda-parameters.dl): `Configure(s => s.AddRules())` with +// `Configure(Action c)` makes `s` an IServiceCollection. Where +// that type is in source, param_type already says so; where it is unstaged, the +// NAME is all there is, and without it an extension written for +// `this IServiceCollection` cannot match the lambda that calls it. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LAMBDA_PARAMETER", p), + lambda_param_arg_ref(prov, p, ar), type_ref_name(prov, ar, tn), tn != "". +// AN IMPLICITLY TYPED LOCAL made by `new T(..)` holds a T, resolved or not: +// `var services = new ServiceCollection(); services.AddRules();`. The name is the +// one written at the creation (external-types.dl labels the boundary from the same +// fact); without it the extension rungs have nothing to compare. +call_recv_type_name(prov, e, tn) :- + call_receiver_expr(prov, e, r), + ref_denotes(prov, r, "LOCAL_VARIABLE", v), var_is_implicit(prov, v), + var_value_initializer(prov, v, init), expr_kind(prov, init, "OBJECT_CREATION"), + call_callee_name(prov, init, tn), tn != "". // A receiver that is itself a call: the callee's declared return type name. The ROOT // reference's name where there is one (`List`, not `List`), which is how every // other receiver is named; the written text only where no reference was recorded. diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index f2ec9440..e7bc391f 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -220,7 +220,8 @@ ref_unbound(prov, e, n) :- !ref_names_type(prov, e, _). // A reference that names a TYPE rather than a value: the receiver of a static call. -// Resolved through type_name_resolves in the file's scope. +// Resolved through type_name_resolves in the file's scope, at the arity written on +// it: `Cache.Get()` names Cache`1, not a non-generic Cache. // // NOT WHERE THE NAME ALSO BINDS TO A VALUE. C# allows a member and a type to share a // name (the language's own "Color Color" rule) and prefers the MEMBER in an @@ -235,10 +236,10 @@ ref_binds_value(prov, e) :- ref_denotes(prov, e, _, _). ref_binds_value(prov, e) :- local_binds(prov, e, _). ref_names_type(prov, e, gk) :- - expr_written_name(prov, e, n), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), expr_ultimate_module(prov, e, mod), !ref_binds_value(prov, e), - type_name_resolves(prov, mod, n, "0", gk). + type_name_resolves(prov, mod, n, ar, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index fb90ddc4..f4d1b803 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -89,6 +89,26 @@ type_name_cand(prov, mod, qn, ar, gk, 1) :- type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk), !type_is_top_level(prov, gk). +// ── A QUALIFIED NAME READ FROM ITS NAMESPACE ──────────────────────────────── +// `Reflection.Info.Create(..)` written inside namespace Shop names +// Shop.Reflection.Info: the first segment of a qualified name is a +// simple name, looked up in the enclosing namespaces like any other. Matched only +// in full, such a receiver stays untyped and every call through it unresolved. +// +// Keyed from the DECLARATION side, so nothing is enumerated per written name: each +// type's qualified name is split at each dot into the namespace before it and the +// dotted tail after it, and the tail is a candidate in every file whose namespace +// chain holds that prefix. A tail with no dot is a simple name, which ranks 2-5 +// already answer. +type_qn_tail(prov, qn, p, s) :- + type_decl(prov, _, qn, _, _, _), + i = range(1, strlen(qn)), substr(qn, i, 1) = ".", + p = substr(qn, 0, i), s = substr(qn, i + 1, strlen(qn) - i - 1), + contains(".", s). +type_name_cand(prov, mod, s, ar, gk, 1) :- + type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). + // ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ────────────────────────────── // Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration // under one `prov`, so a client module and a library namespace are never brought @@ -211,10 +231,10 @@ type_ref_demand(prov, mod, n, ar) :- // a type name and produced `external:s.Describe` -- an external target named after a // local variable, which is a wrong answer rather than a missing one. The !ref_denotes // term is what makes the guard "unbound by anyone". -type_ref_demand(prov, mod, n, "0") :- +type_ref_demand(prov, mod, n, ar) :- call_receiver_expr(prov, e, r), expr_ref_unknown(prov, r), - expr_written_name(prov, r, n), + expr_written_name(prov, r, n), expr_type_argc(prov, r, ar), !ref_denotes(prov, r, _, _), call_module(prov, e, mod). diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 2bb9352f..89a6ff36 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -67,6 +67,7 @@ .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) .decl call_chain_summary(c0:symbol,c1:number) .decl call_class(c0:symbol,c1:symbol,c2:symbol) +.decl call_class_kind(c0:symbol) .decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl call_ctor_implicit(c0:symbol,c1:symbol,c2:symbol) .decl call_ctor_target(c0:symbol,c1:symbol,c2:symbol) @@ -264,6 +265,7 @@ .decl ctl_route_c(c0:symbol,c1:symbol,c2:symbol) .decl ctl_route_tok(c0:symbol,c1:symbol,c2:symbol) .decl ctor_accepts_argc(c0:symbol,c1:symbol,c2:symbol) +.decl ctor_has_initializer(c0:symbol) .decl ctor_implicit_type(c0:symbol,c1:symbol) .decl decl_return_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl delegate_arg_expr(c0:symbol,c1:symbol) @@ -358,6 +360,7 @@ .decl expr_target_any(c0:symbol,c1:symbol,c2:symbol) .decl expr_target_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_type(c0:symbol,c1:symbol,c2:symbol) +.decl expr_type_argc(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_owner(c0:symbol,c1:symbol,c2:symbol) .decl expr_type_via_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_ultimate_method(c0:symbol,c1:symbol,c2:symbol) @@ -486,8 +489,10 @@ .decl implements_member(c0:symbol,c1:symbol,c2:symbol) .decl implements_params_conflict(c0:symbol,c1:symbol) .decl implements_shape(c0:symbol,c1:symbol,c2:symbol) +.decl implicit_base_ctor(c0:symbol,c1:symbol) .decl implicit_base_group(c0:symbol,c1:symbol) .decl implicit_base_name(c0:symbol) +.decl implicit_base_site(c0:symbol,c1:symbol) .decl implicit_new_untargeted(c0:symbol,c1:symbol,c2:symbol) .decl indexer_access(c0:symbol,c1:symbol,c2:symbol) .decl indexer_getter(c0:symbol,c1:symbol,c2:symbol) @@ -662,6 +667,7 @@ .decl module_lang(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_namespace_own(c0:symbol,c1:symbol,c2:symbol) .decl module_namespace_style(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl module_ns_chain(c0:symbol,c1:symbol,c2:symbol) .decl module_parse_errors(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_regime(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -947,6 +953,7 @@ .decl type_parent(c0:symbol,c1:symbol,c2:symbol) .decl type_placement(c0:symbol,c1:symbol,c2:symbol) .decl type_primary_ctor(c0:symbol,c1:symbol,c2:symbol) +.decl type_qn_tail(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) .decl type_ref_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_argc(c0:symbol,c1:symbol,c2:symbol) diff --git a/graph/java/engine/expression-resolution/expr-type.dl b/graph/java/engine/expression-resolution/expr-type.dl index 94366240..054c50df 100644 --- a/graph/java/engine/expression-resolution/expr-type.dl +++ b/graph/java/engine/expression-resolution/expr-type.dl @@ -649,16 +649,10 @@ recv_decl_ref(recv, retRef, recvType) :- applicable_candidate(recv, innerCallee) // declared-type clause above yields a reference carrying no arguments. The initializer's callee // return type carries them. The field analogue is unnecessary: Java has no `var` fields, so a // field always writes its type. -recv_decl_ref(recv, retRef, t) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv), - expr_ultimate_method("client", recv, m), - java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), - java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr), +recv_decl_ref(recv, retRef, t) :- local_use_init(recv, initExpr), applicable_candidate(initExpr, callee), method_return_type_resolves(_, callee, retRef, t). -recv_decl_ref(recv, retRef, t) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv), - expr_ultimate_method("client", recv, m), - java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), - java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr), +recv_decl_ref(recv, retRef, t) :- local_use_init(recv, initExpr), applicable_candidate(initExpr, callee), lib_type_reference(_, "METHOD_RETURN", _, _, _, _, _, "0", _, _, _, _, _, _, _, callee, "METHOD", retRef), lib_type_ref_resolves(_, retRef, t). diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl index 77697803..4b83b6a1 100644 --- a/graph/java/engine/resolution/external-types.dl +++ b/graph/java/engine/resolution/external-types.dl @@ -178,9 +178,8 @@ expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, nam // with `Format source()` unstaged), the local is that external type exactly as `Format f` would // be. Without it the receiver has no external type at all and the call is a declared unknown. // java_local_variable (19): 11 isVarInferred 13 method 18 hash -expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, e), - expr_ultimate_method("client", e, m), - java_local_variable(name, _, _, _, _, _, _, _, _, _, _, "true", _, m, _, _, _, _, local), +expr_type("external", e, x) :- local_use(e, local), + java_local_variable(_, _, _, _, _, _, _, _, _, _, _, "true", _, _, _, _, _, _, local), local_init_expr(local, init), expr_type("external", init, x). expr_type("external", e, x) :- java_expression("CAST_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), diff --git a/graph/java/engine/resolution/generic-chain.dl b/graph/java/engine/resolution/generic-chain.dl index 5cf2ba5f..a9e35fae 100644 --- a/graph/java/engine/resolution/generic-chain.dl +++ b/graph/java/engine/resolution/generic-chain.dl @@ -100,10 +100,7 @@ expr_arg_binding(call, nested, nestedParam, arg) :- call_site(call, _, recv), // the method it actually appears in — same guard the recv_decl_ref clauses in expr-type.dl use. // java_expression (25): 11 name 15 referenceKind 25 hash. java_local_variable (19): 1 name 14 method 19 hash. expr_arg_binding(recv, bt, param, arg) :- - java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv), - expr_ultimate_method("client", recv, m), - java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), - java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr), + local_use_init(recv, initExpr), expr_arg_binding(initExpr, bt, param, arg). // ── INHERITED generics with a CONCRETE super-arg (c9): `class DogBox extends Box`. diff --git a/graph/java/engine/resolution/local-flow.dl b/graph/java/engine/resolution/local-flow.dl index 0d935863..8680ccab 100644 --- a/graph/java/engine/resolution/local-flow.dl +++ b/graph/java/engine/resolution/local-flow.dl @@ -33,6 +33,20 @@ // java_local_variable (19): 0 name 13 method 18 hash // ============================================================================ +// A USE of a local: the LOCAL_VARIABLE reference joined to its declaration, ONCE, below the +// typing fixpoint. These same three atoms sat inline in five recursive rules, and semi-naive +// evaluation re-ran the full java_expression scan every iteration — 163 iterations on a large +// subject, ~40% of the whole solve between them. Hoisted, each of those rules starts from its +// recursive delta and probes this table. +local_use(use, local) :- + java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, use), + expr_ultimate_method("client", use, m), + java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local). +// ...and the uses whose local is initialized by a call, with that initializer. +local_use_init(use, initExpr) :- + local_use(use, local), + java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr). + // The initializer expression of a local (LOCAL_VAR_INITIALIZER root, owned by the local). local_init_expr(local, e) :- java_expression(_, "ROOT", "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e). @@ -53,7 +67,5 @@ local_flow_type(local, t) :- local_assign_value(local, e), expr_type(_, e, t). // PROPAGATE: a LOCAL_VARIABLE receiver reference takes the flow-type(s) of its local // (POSITIVE — resolves the concrete method directly; the cap handles the residual fan). expr_type("client", recv, t) :- - java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv), - expr_ultimate_method("client", recv, m), - java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), + local_use(recv, local), local_flow_type(local, t). diff --git a/graph/java/souffle/decls_all.dl b/graph/java/souffle/decls_all.dl index 57f51bdd..605f86b2 100644 --- a/graph/java/souffle/decls_all.dl +++ b/graph/java/souffle/decls_all.dl @@ -475,6 +475,8 @@ .decl cha_wide_base(c0:symbol) .decl cha_override_low(c0:symbol,c1:symbol) .decl local_init_expr(c0:symbol,c1:symbol) +.decl local_use(c0:symbol,c1:symbol) +.decl local_use_init(c0:symbol,c1:symbol) .decl local_assign_value(c0:symbol,c1:symbol) .decl local_flow_type(c0:symbol,c1:symbol) .decl param_flow_type(c0:symbol,c1:symbol) diff --git a/graph/javascript/README.md b/graph/javascript/README.md index 6a26b2ab..1336eecd 100644 --- a/graph/javascript/README.md +++ b/graph/javascript/README.md @@ -103,6 +103,7 @@ arrays. That is the engine, entirely. | module graph | `resolution/module-graph.dl` | one export surface for both systems, keyed by name with `default` for `module.exports = X`; a CommonJS default value's properties ARE its members | | hierarchy | `resolution/type-hierarchy.dl` | ONE closure — every heritage form inherits members, there is no `implements` | | value flow | `resolution/value-flow.dl` | the may-analysis above | +| instance state | `resolution/instance-state.dl` | an `("alloc", new-expression)` value beside `("inst", T)`, and the facts that let impact walk a callback or dependency given to ONE instance (a constructor option, a subscription) only from callers whose receiver may be that instance | | arrays | `resolution/arrays.dl` | the one platform type modelled: `push`, `[i]`, `map`, `forEach`, `for..of`, `T[]`; `Map` / `Set` as collections, including an instance of a class that extends one (#619) | | ambient | `resolution/ambient.dl` | platform names as values, so a site reached through one is classified from the value, not the syntax | | JSDoc types | `resolution/reference-types.dl` | `@param`/`@type`/`@returns`, `import()` types, typedef aliases, wrappers | diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index a257ba73..5745b29d 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -72,6 +72,36 @@ unresolved_covered(ce) :- call_site("client", ck, _, _, _, _, ce, _, _), call_ki unresolved_receiver(ce, cn, ck, "other_form") :- call_site("client", ck, cn, _, _, _, ce, _, _), !call_resolved(ce), !unresolved_covered(ce). +// ── library_receiver(CallExpr, Specifier) ─────────────────────────────────── +// An unresolved member call whose receiver is a PACKAGE'S value: a binding imported +// from a package no staged IR declares, a member of one, what calling or constructing +// one returns, and a const holding any of those — `request(app).get('/x').expect(200)`, +// `new grpc.Metadata().get(k)`, `const md = new Metadata(); md.get(k)`. The receiver's +// origin is known, so the site is not an untyped `x.get()`: a project method of the +// same name is not what it calls. It stays unresolved (the package is not staged), and +// consumers list it apart from the name matches on unknown receivers rather than +// among them. Read off the source, never the value flow, so it cannot invent a value. +// A call handed a project INSTANCE is left out: a wrapper, a proxy or a mock helper +// may return the very object it was given (`mocked(repo).get(1)`). +// A package is named by a bare specifier: a relative or absolute path that reached no +// file (`require('./not-here')`) is a missing PROJECT module, and an alias (`@/x`, `~/x`, +// `#x`) may map onto the project's own source. +package_import(imp, spec) :- import_decl(_, spec, _, _, _, _, out, _, imp), import_outcome_is_package(out), + !match("[.#~/].*", spec), !match("@/.*", spec). +library_value(e, spec) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + package_import(imp, spec). +library_value(e, spec) :- expr_kind(_, "MODULE_EDGE_CALL", _, e), expr_module_edge(_, imp, e), package_import(imp, spec). +library_value(e, spec) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_child(_, e, "ACCESS_TARGET", _, r), + library_value(r, spec). +library_value(e, spec) :- expr_kind(_, k, _, e), (k = "CALL" ; k = "NEW"), expr_child(_, e, role, _, c), + (role = "CALLEE" ; role = "RECEIVER"), library_value(c, spec), !call_passes_project_instance(e). +library_value(e, spec) :- expr_kind(_, "AWAIT", _, e), expr_child(_, e, _, _, c), library_value(c, spec). +library_value(e, spec) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), !var_import(_, _, v), + var_init("client", _, i, v), !variable_reassigned(v), library_value(i, spec). +call_passes_project_instance(e) :- call_arg(e, _, a), expr_value(a, "inst", _). +library_receiver(ce, spec) :- call_site("client", ck, _, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), + !call_resolved(ce), expr_child(_, ce, "RECEIVER", _, r), library_value(r, spec). + // ── unresolved_value_callee(CallExpr, Binding) ────────────────────────────── // An unresolved call whose callee is a VALUE, not a name: `cb()` on a parameter, // `f()` on a loop variable or a local, `table[k]()`, `eval(s)`. Nothing about its @@ -140,6 +170,29 @@ call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :- wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). variable_reassigned(v) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt). +// ── a call of what a PROJECT wrapper returned runs what that site was handed ── +// `export default promisable(eachItem, 3)`, `const add = curry2(function add(a, b) {…})`: +// the wrapper returns a closure that calls its parameter, and every export of the module +// is that closure. The closure's call of the parameter is read context-insensitively, so +// it held every function any of the wrapper's sites passed (past the fan cap: nothing), +// and a test calling the export reached the closure and stopped there. The site is +// already a value of its own, ("wrap", site), beside the closure (value-flow.dl), and a +// call through it takes its RESULT from that site's argument alone; the same site says +// what the call RUNS: the function it was handed at the forwarded position, what the +// wrapper writes into that parameter, or what a wrapped argument itself runs +// (`memoize(once(f))`). One of a set, beside the edge to the closure the call keeps, and +// under the same fan cap: a callee that may hold more wrapped values than --dispatch-cap +// (a parameter every wrapped export is passed to) runs one of them, and naming all of +// them says nothing. +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "func", g). +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "wrap", s2), wrap_runs(s2, g). +wrap_runs(s, g) :- wrap_site(s, h, pos), wrapper_param_written(h, pos, o), expr_value(o, "func", g). +wrapped_call_target(ce, g) :- callee_value(ce, "wrap", s), wrap_runs(s, g), method_prov(g, "client"), + !call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _). +wrapped_target_count(ce, n) :- wrapped_call_target(ce, _), n = count : { wrapped_call_target(ce, _) }. +wrapped_over_cap(ce) :- wrapped_target_count(ce, n), dispatch_cap(c), n > to_number(c). +call_chain_edge(ce, caller, "-", g, "client", "multi_inferred", kind) :- + wrapped_call_target(ce, g), !call_over_cap(ce), !wrapped_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). // `(c ? a : b)()`, `(0, cb)()`, `make()()`: the callee is computed by an expression. // `new Function(s)()` / `Function(s)()` runs code built at run time. unresolved_value_callee(ce, "dynamic_code") :- value_callee_unresolved(ce, c), callee_builds_code(c). diff --git a/graph/javascript/engine/call-edge-generation/field_access.dl b/graph/javascript/engine/call-edge-generation/field_access.dl new file mode 100644 index 00000000..066a5aee --- /dev/null +++ b/graph/javascript/engine/call-edge-generation/field_access.dl @@ -0,0 +1,60 @@ +// ============================================================================ +// CALL-EDGE-GEN · A MODULE VARIABLE READ BY NAME, AS A DATA EDGE +// +// `CFG.name`, `use(SCHEMA)`, `${TOKENS.store}`: the identifier is bound by the binder +// (expr_binding) to a variable -- the module's own top-level one, or an import binding. +// This engine follows an import to the VALUE the export holds (import_value), which is +// what a call needs, and never to the exported VARIABLE, which is what "who reads this +// const" needs. So a read of an imported const was answered by name, and the name +// matched every same-named const in every sibling package. +// +// The export surface is walked here once more, keeping the variable: export_decl names +// it (`export const x` and `export { x }` carry target kind VARIABLE), and a re-export +// (`export * from`, `export { x } from`) passes it on exactly as module_export_value +// passes a value on. A variable holding a function or a class is left out: its uses +// are calls and constructions, which call edges already carry. +// +// field_access(Site, Caller, Variable, "client", "known_edge", Access), the shape the +// Java and TypeScript engines export, so the bundle reads one relation for all three. +// ============================================================================ + +js_module_var(v) :- var_decl("client", _, reg, _, _, _, v), !var_owner_method("client", _, v), + !js_not_a_data_binding(reg), + !var_init("client", "FUNCTION", _, v), !var_init("client", "CLASS", _, v), + !var_import("client", _, v). +js_not_a_data_binding("IMPORT_BINDING"). +js_not_a_data_binding("FUNCTION_DECLARATION_HOISTED"). +js_not_a_data_binding("CLASS_TDZ"). +js_not_a_data_binding("CATCH_PARAMETER"). + +// ── export_var(Module, ExportedName, Variable) — the surface, by variable ─── +export_var(mod, n, v) :- export_decl("client", n, _, _, _, "VARIABLE", v, _, mod, _), n != "", js_module_var(v). +export_var(mod, n, v) :- export_decl("client", en, _, "EXPORT_ALL", _, _, _, _, mod, x), export_all_is_spread(en), + export_reexport(_, _, imp, x), import_module(imp, src), + export_var(src, n, v), n != "default". +export_var(mod, en, v) :- export_decl("client", en, ln, form, _, _, _, _, mod, x), form != "EXPORT_ALL", + export_reexport(_, _, imp, x), import_module(imp, src), ln != "", + export_var(src, ln, v). +// ── import_var(ImportHash, Variable) — what a named import binding stands for ─ +import_var(imp, v) :- import_decl("client", _, _, bf, n, _, _, _, imp), import_binding_is_named(bf), n != "", + !contains(".", n), import_module(imp, mod), export_var(mod, n, v). + +// ── the site ──────────────────────────────────────────────────────────────── +js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", v, e), js_module_var(v). +js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", lv, e), + var_import("client", imp, lv), import_var(imp, v). +// A plain `=` never reads the old value; `+=` and `++` read and then write. +js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, e). +js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "UNARY", _, u), expr_operator(_, op, u), update_operator(op), + expr_child(_, u, _, _, e). +js_var_write_only(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, "=", a), + expr_child(_, a, "ASSIGNMENT_TARGET", _, e). +js_var_access(e, "write") :- js_var_written(e), js_var_write_only(e). +js_var_access(e, "readwrite") :- js_var_written(e), !js_var_write_only(e). +js_var_access(e, "read") :- js_var_read_target(e, _), !js_var_written(e). +// the caller: the callable the expression is written in, else the module initializer +js_var_read_from(e, m) :- js_var_read_target(e, _), expr_owner("client", m, _, e), m != "". +js_var_read_from(e, init) :- js_var_read_target(e, _), expr_owner("client", "", mod, e), module_init(_, init, mod). + +field_access(e, caller, v, "client", "known_edge", acc) :- js_var_read_target(e, v), + js_var_read_from(e, caller), js_var_access(e, acc). diff --git a/graph/javascript/engine/resolution/fan-cap.dl b/graph/javascript/engine/resolution/fan-cap.dl index af6bdd93..ad69acdb 100644 --- a/graph/javascript/engine/resolution/fan-cap.dl +++ b/graph/javascript/engine/resolution/fan-cap.dl @@ -80,14 +80,48 @@ direct_export_func(mod, n, m) :- export_decl(_, n, _, _, _, _, _, se, mod, _), s // shares its name with hundreds of unrelated sites and must not be capped by them, // while a module-level `each` or `merge` called under its own name from everywhere // is exactly the generic utility the cap exists for. +// +// The PROGRAM's arguments are decided by the program's sites alone. A suite calls the +// API it tests over and over, and counted with the program those calls made exactly the +// functions under test "generic utilities": their parameters went untracked, so a +// callback the program hands a constructor and stores on the instance (`this.decode = +// options.decode`) was lost, and the program's own call through that field resolved only +// to the fallback beside it. So there are two verdicts: +// method_is_hot every site counts (as before): decides whether a TEST's +// arguments flow in +// method_is_hot_in_program test files' sites do not count: decides whether the +// PROGRAM's arguments flow in +// Nothing that flowed before stops flowing (a test handing the program a plugin it calls +// back still reaches it). A function hot only because of its tests now receives what the +// program passes it and still nothing from the tests: their callbacks would be the widest +// values it holds, and with them the call through the stored field would be over the +// dispatch cap and answer nothing. param_flow_capped_at(n) :- dispatch_cap(s), n = to_number(s). direct_call_count(m, n) :- direct_call_site(_, m), n = count : { direct_call_site(_, m) }. method_is_hot(m, n) :- direct_call_count(m, n), param_flow_capped_at(c), n > c. name_call_count(cn, n) :- call_site(_, _, cn, _, _, _, _, _, _), cn != "", n = count : { call_site(_, _, cn, _, _, _, _, _, _) }. +program_direct_site(ce, m) :- direct_call_site(ce, m), !site_in_test_module(ce). +program_direct_count(m, n) :- program_direct_site(_, m), n = count : { program_direct_site(_, m) }. +method_is_hot_in_program(m, n) :- program_direct_count(m, n), param_flow_capped_at(c), n > c. +// The NAME count is the overcount for module-level functions. A test's member call that +// only shares the name (`lib.add(1, 2)` on an export that is a different function, a +// curried closure) stays in it, as before; a test's bare call or `new` of the name +// (`build(...)`, `new lib.Codec(...)`) denotes the function itself and leaves it. +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck). +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, "CONSTRUCTOR_CALL", _, _, _, _, ce, _, _). +program_name_site(m, ce) :- module_level_function(m, cn), call_site(_, _, cn, _, _, _, ce, _, _), !test_names_itself(ce). +program_name_count(m, n) :- program_name_site(m, _), n = count : { program_name_site(m, _) }. +// ── test_module / site_in_test_module ────────────── +// By path, as runners find their files: a `test`/`tests`/`__tests__`/`spec(s)` directory, +// or a `.test.` / `.spec.` file. +test_module(mod) :- module_file("client", fp, mod), + match("(.*/)?(tests?|__tests__|specs?)/.*|.*[.](test|spec)[.][cm]?[jt]sx?", fp). +site_in_test_module(ce) :- expr_owner(_, _, mod, ce), test_module(mod). module_level_function(m, n) :- method_decl(_, n, "FUNCTION_DECLARATION", _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). module_level_function(m, n) :- var_decl(_, n, _, _, _, mod, v), function_binding_value(v, m), method_decl(_, _, _, _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). method_is_hot(m, n) :- module_level_function(m, cn), name_call_count(cn, n), param_flow_capped_at(c), n > c. +method_is_hot_in_program(m, n) :- program_name_count(m, n), param_flow_capped_at(c), n > c. // ── param_fan_capped(ParamHash, SiteCount) — countable ────────────────────── -param_fan_capped(p, n) :- method_is_hot(m, n), param_decl(_, _, _, m, p). +param_fan_capped(p, n) :- method_is_hot_in_program(m, n), param_decl(_, _, _, m, p). diff --git a/graph/javascript/engine/resolution/instance-state.dl b/graph/javascript/engine/resolution/instance-state.dl new file mode 100644 index 00000000..df747c53 --- /dev/null +++ b/graph/javascript/engine/resolution/instance-state.dl @@ -0,0 +1,133 @@ +// ============================================================================ +// Resolution · INSTANCE STATE (what ONE object was given, not what its class was) +// +// `new Bus({ validate: check })` and `b.on(handleA)` hand a callback to ONE bus. The +// flow layer keys an instance by its class, ("inst", Bus), so `this.v` inside +// `Bus.emit` holds every callback any bus was ever given, and the call edge +// emit -> check is right for SOME bus. The edge is right; what is wrong is reading it +// as "every caller of emit reaches check": a caller that emits on a bus built without +// `validate` does not. A context-insensitive call graph cannot say that, so this file +// says it beside the graph, as facts the impact walk reads (dl/impact.dl): +// +// state_world(M, T) callable M runs with an instance of T as its `this`: +// an instance member of T, or a function nested in one +// state_gate(M, F, T) every call M makes to F reaches F only through what +// the receiver instance was given +// state_gate_alloc(T, F, S) the allocation S (a `new T(...)` expression) is one +// that was given F +// state_call_alloc(C, M, S) C calls the T member M on a receiver that may be S +// state_call_open(C, M) C calls M on a receiver whose allocation is unknown +// +// ── THE ALLOCATION VALUE ──────────────────────────────────────────────────── +// ("alloc", NewExpr) is a value of its own, carried BESIDE ("inst", T) wherever the +// instance goes (a variable, a parameter, a field, a return, `x ?? new T()`), exactly +// as ("wrap", site) travels beside a wrapper's closure (value-flow.dl). Nothing +// resolves through it: members are still read off ("inst", T). It is minted only for +// a class whose instance code CALLS A VALUE — a parameter (`f(e)`, `handler(env)`) or +// a property of `this` that the class does not declare as a method (`this.v?.(e)`) — +// which is the shape whose targets depend on what the instance was given. Every +// other class carries no allocation, and so costs nothing here. +// +// ── WHEN A CALLBACK IS GATED ──────────────────────────────────────────────── +// F is gated in T when the only way F enters T's instance code is as an argument of +// a call made from OUTSIDE that code on a T member (the constructor or a method) — +// an ENTRY — and every such entry's receiver has a known allocation. It is not gated +// (every caller keeps it, as before) when F: +// · is declared inside T's instance code; +// · is read inside that code through a name bound outside it (an import, a +// module-level variable, a function declaration), or returned to it by a call +// to a function declared outside it; +// · is written onto an instance of T, or onto the static side of T, from outside; +// · reaches T whose hierarchy has a client class above or below it, or whose own +// code builds another T (`new T()`, `new this.constructor()`): what one instance +// holds could then flow into another; +// · enters through an entry whose receiver's allocation is unknown. +// The allocations are the closed-world answer the engine gives for parameters too: a +// receiver's allocations are the `new` expressions that reach it through resolved +// flow. A receiver with none (a documented `@param {Bus}`, a library's return) is open. +// ============================================================================ + +// ── the instance code of a class ──────────────────────────────────────────── +state_world(m, t) :- method_decl("client", _, k, _, "false", t, _, m), t != "", k != "STATIC_BLOCK". +state_world(m, t) :- type_ctor("client", m, t). +state_world(m, t) :- method_enclosing(m, e), state_world(e, t). +state_type_declares(t, n) :- method_decl(_, n, _, _, _, t, _, _), n != "". + +// ── the classes whose instance code calls a value ─────────────────────────── +state_type(t) :- state_world(em, t), call_site("client", ck, n, "SYNTACTIC", _, em, ce, _, _), call_kind_is_member_form(ck), + expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "THIS", _, r), n != "", !state_type_declares(t, n). +state_type(t) :- state_world(em, t), call_site("client", ck, _, _, _, em, ce, _, _), call_kind_is_callee_form(ck), + expr_child(_, ce, "CALLEE", _, c), expr_param(_, _, c). + +// ── the allocation value, minted at `new T(...)` ──────────────────────────── +expr_value(e, "alloc", e) :- expr_kind("client", "NEW", _, e), new_constructs(e, t), state_type(t). +state_alloc_type(s, t) :- expr_value(s, "alloc", s), new_constructs(s, t). + +// ── eligibility: nothing one instance holds can move into another ─────────── +state_type_mixed(t) :- state_type(t), type_super(t, s), type_decl("client", _, _, _, _, s). +state_type_mixed(t) :- state_type(t), type_super(u, t), type_decl("client", _, _, _, _, u). +state_type_mixed(t) :- state_type(t), state_world(em, t), expr_kind(_, "NEW", _, e), expr_owner(_, em, _, e), new_constructs(e, t). +state_type_mixed(t) :- state_type(t), state_world(em, t), own_class_new(em, _). +state_type_ok(t) :- state_type(t), !state_type_mixed(t). + +// ── entries: a call from outside the instance code onto a member of T ─────── +state_entry(t, ce) :- state_type_ok(t), expr_resolves_to_method(ce, m), state_world(m, t), + call_site("client", _, _, _, _, em, ce, _, _), !state_world(em, t). +state_entry_alloc(ce, s) :- state_entry(t, ce), expr_kind(_, "NEW", _, ce), expr_value(ce, "alloc", s), state_alloc_type(s, t). +state_entry_alloc(ce, s) :- state_entry(t, ce), expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "alloc", s), state_alloc_type(s, t). +state_entry_open(ce) :- state_entry(_, ce), !state_entry_alloc(ce, _). + +// What an entry's arguments carry: the values themselves and, a few levels down, what +// their properties and elements hold (`{ validate: check }`, `[a, b]`). Depth-bounded: +// a carry is evidence FOR gating, so a callback found deeper than this is simply not +// counted as entering through the entry, and then it is not gated at all. +state_carry(ce, k, i, 0) :- state_entry(_, ce), call_arg(ce, _, a), expr_value(a, k, i), k != "alloc". +state_carry(ce, k2, i2, d + 1) :- state_carry(ce, k, i, d), d < 3, k != "func", prop_value(k, i, _, k2, i2), k2 != "alloc". +state_carry(ce, k2, i2, d + 1) :- state_carry(ce, k, i, d), d < 3, (k = "arr" ; k = "coll"), elem_value(i, k2, i2), k2 != "alloc". +state_enters(t, f, ce) :- state_entry(t, ce), state_carry(ce, "func", f, _). + +// ── leaks: F reaches the instance code some other way ─────────────────────── +// declared inside it +state_leak(t, f) :- state_enters(t, f, _), state_world(f, t). +// a value the instance code has from anywhere but an entry: a name bound outside it +// (an import, a module-level variable or function, a class it constructs itself: `dep ?? +// new DefaultDep()` names DefaultDep, whose prototype holds `run`), or what a function +// declared outside it returns. What those hold, a few levels down, is F leaking in. A +// literal the code writes itself needs no rule: a function in it is either named (above) +// or written inline, and then it is declared inside the instance code. +state_outer_ref(t, e) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, e), expr_binding(_, v, e), + var_owner_method(_, vm, v), !state_world(vm, t). +state_outer_ref(t, e) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, e), expr_binding(_, v, e), + !var_owner_method(_, _, v). +state_outer_ref(t, ce) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, ce), expr_kind(_, "CALL", _, ce), + expr_resolves_to_method(ce, g), !state_world(g, t). +state_outer_carry(t, k, i, 0) :- state_outer_ref(t, e), expr_value(e, k, i), k != "alloc". +state_outer_carry(t, k2, i2, d + 1) :- state_outer_carry(t, k, i, d), d < 3, k != "func", prop_value(k, i, _, k2, i2), k2 != "alloc". +state_outer_carry(t, k2, i2, d + 1) :- state_outer_carry(t, k, i, d), d < 3, (k = "arr" ; k = "coll"), elem_value(i, k2, i2), k2 != "alloc". +state_leak(t, f) :- state_enters(t, f, _), state_outer_carry(t, "func", f, _). +// written onto an instance of T or onto T itself from outside its instance code +// The assignment side enumerated ONCE, keyed (t, f): anchored on state_enters the +// clause crossed every (type, entry) pair with every assignment in the program — 33s +// for 2 rows on the profile. +func_written_onto(t, f, em) :- expr_kind(_, "ASSIGNMENT", _, a), expr_owner(_, em, _, a), + expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_child(_, tgt, "ACCESS_TARGET", _, r), + expr_value(r, k, t), (k = "inst" ; k = "ctor"), + expr_child(_, a, "ASSIGNMENT_VALUE", _, val), expr_value(val, "func", f). +state_leak(t, f) :- state_enters(t, f, _), func_written_onto(t, f, em), !state_world(em, t). +state_leak(t, f) :- state_enters(t, f, _), prop_value("ctor", t, _, "func", f). +// an entry that says nothing about which instance it gives F to +state_leak(t, f) :- state_enters(t, f, ce), state_entry_open(ce). + +// ── the gate ──────────────────────────────────────────────────────────────── +state_gated(t, f) :- state_enters(t, f, _), !state_leak(t, f). +state_gate_alloc(t, f, s) :- state_gated(t, f), state_enters(t, f, ce), state_entry_alloc(ce, s). +state_gate(m, f, t) :- state_gated(t, f), state_world(m, t), expr_resolves_to_method(ce, f), + call_site("client", _, _, _, _, m, ce, _, _). + +// ── who calls the instance code, on which allocation ──────────────────────── +state_gated_type(t) :- state_gated(t, _). +state_call_alloc(c, m, s) :- state_gated_type(t), state_entry(t, ce), expr_resolves_to_method(ce, m), + call_site("client", _, _, _, _, c, ce, _, _), state_entry_alloc(ce, s). +state_call_open(c, m) :- state_gated_type(t), state_entry(t, ce), expr_resolves_to_method(ce, m), + call_site("client", _, _, _, _, c, ce, _, _), state_entry_open(ce). +state_world_of_gated(m, t) :- state_gated_type(t), state_world(m, t). diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index d6f8c132..e78863e2 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -264,8 +264,15 @@ param_value(p, "inst", t) :- param_type_name(_, _, tr, p), type_ref_type(tr, t). // values on every export, a 20-minute solve — because a generic library is exactly // the shape whose parameters unify everything. What a library does with a callback it // is handed is a MODEL (resolution/frameworks.dl), stated per library, not inferred. -param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +// The program's sites pass their arguments unless the PROGRAM calls the function from +// more places than the cap; a test file's sites only while every site together stays +// under it (fan-cap.dl). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), + expr_value(arg, k, i). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), expr_value(arg, k, i). // THE ONE LIBRARY EXCEPTION: a UMD wrapper (#710). `(function (root, factory) { @@ -283,8 +290,12 @@ param_value(p, "func", f) :- call_site(_, "IIFE_CALL", _, _, _, _, ce, _, _), ca expr_value(arg, "func", f). // A rest parameter IS an array; the arguments at and past its position are its elements. param_value(p, "arr", p) :- param_is_rest(_, p). -elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, + expr_value(arg, k, i). +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, expr_value(arg, k, i). // A default value: `cb = () => {}`. The parser gives the parameter the default's @@ -493,8 +504,13 @@ carry_step(x, c) :- expr_kind(_, "BINARY", _, x), expr_operator(_, op, x), binar carry_step(x, c) :- expr_kind(_, "ASSIGNMENT", _, x), expr_child(_, x, "ASSIGNMENT_VALUE", _, c). carry_step(x, c) :- expr_kind(_, "SEQUENCE", _, x), sequence_last(x, c). carry_step(x, c) :- expr_binding(_, v, x), var_init(_, _, c, v). -carry_step(x, c) :- expr_binding(_, v, x), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op), +// An assignment writing variable v the value c, KEYED BY THE VARIABLE and enumerated +// once. Anchored on expr_binding the clause below visited every USE of every variable +// and re-scanned the assignments for each: 3.1 minutes of a 6-minute solve, half the +// profile, for 16.5K rows. +var_assigned_value(v, c) :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt), expr_child(_, a, "ASSIGNMENT_VALUE", _, c). +carry_step(x, c) :- expr_binding(_, v, x), var_assigned_value(v, c). carry_rest(e, x) :- carry_part(e, x), !carries_param_call(x, _). // What the carrying return holds read context-insensitively: every caller's argument // merged (and nothing once the wrapper is over the fan cap). Only the fallbacks below diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 7f73c459..bc489cb6 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -87,6 +87,10 @@ .decl call_resolved(c0:symbol) .decl unresolved_receiver(c0:symbol, c1:symbol, c2:symbol, c3:symbol) .decl unresolved_covered(c0:symbol) +.decl package_import(c0:symbol, c1:symbol) +.decl library_value(c0:symbol, c1:symbol) +.decl call_passes_project_instance(c0:symbol) +.decl library_receiver(c0:symbol, c1:symbol) .decl value_callee_open(c0:symbol, c1:symbol) .decl unresolved_value_callee(c0:symbol, c1:symbol) .decl callee_builds_code(c0:symbol) @@ -108,6 +112,10 @@ .decl wrapper_holder_call(c0:symbol, c1:symbol) .decl wrapper_runs(c0:symbol, c1:symbol) .decl wrapper_call_target(c0:symbol, c1:symbol) +.decl wrap_runs(c0:symbol, c1:symbol) +.decl wrapped_call_target(c0:symbol, c1:symbol) +.decl wrapped_target_count(c0:symbol, c1:number) +.decl wrapped_over_cap(c0:symbol) .decl live_export_variable(c0:symbol, c1:symbol) .decl this_type_open(c0:symbol) @@ -232,6 +240,18 @@ .decl var_owner_method(c0:symbol, c1:symbol, c2:symbol) .decl var_type_name(c0:symbol, c1:symbol, c2:symbol, c3:symbol) .decl var_pattern_root(c0:symbol, c1:symbol, c2:symbol) + +// ── call-edge-generation/field_access.dl ── +.decl js_module_var(c0:symbol) +.decl js_not_a_data_binding(c0:symbol) +.decl export_var(c0:symbol, c1:symbol, c2:symbol) +.decl import_var(c0:symbol, c1:symbol) +.decl js_var_read_target(c0:symbol, c1:symbol) +.decl js_var_written(c0:symbol) +.decl js_var_write_only(c0:symbol) +.decl js_var_access(c0:symbol, c1:symbol) +.decl js_var_read_from(c0:symbol, c1:symbol) +.decl field_access(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol, c5:symbol) .decl var_binding_form(c0:symbol, c1:symbol, c2:symbol) // ── resolution/module-graph.dl ── @@ -368,6 +388,7 @@ .decl wrapper_param_replaced(c0:symbol, c1:number) .decl wrapper_param_written(c0:symbol, c1:number, c2:symbol) .decl carry_part(c0:symbol, c1:symbol) +.decl var_assigned_value(c0:symbol,c1:symbol) .decl carry_step(c0:symbol, c1:symbol) .decl carry_rest(c0:symbol, c1:symbol) .decl carried_value(c0:symbol, c1:symbol, c2:symbol) @@ -445,6 +466,14 @@ .decl param_fan_capped(c0:symbol, c1:number) .decl import_binding_is_default_like(c0:symbol) .decl name_call_count(c0:symbol, c1:number) +.decl test_module(c0:symbol) +.decl site_in_test_module(c0:symbol) +.decl program_direct_site(c0:symbol, c1:symbol) +.decl program_direct_count(c0:symbol, c1:number) +.decl method_is_hot_in_program(c0:symbol, c1:number) +.decl test_names_itself(c0:symbol) +.decl program_name_site(c0:symbol, c1:symbol) +.decl program_name_count(c0:symbol, c1:number) .decl module_level_function(c0:symbol, c1:symbol) .decl ambient_collection(c0:symbol) .decl coll_method_call(c0:symbol, c1:symbol, c2:symbol) @@ -569,3 +598,27 @@ // ── resolution/ambient.dl (the global object) ── .decl global_object_name(c0:symbol) .decl global_name_resolution(c0:symbol) + +// ── resolution/instance-state.dl ── +.decl state_world(c0:symbol, c1:symbol) +.decl state_type_declares(c0:symbol, c1:symbol) +.decl state_type(c0:symbol) +.decl state_alloc_type(c0:symbol, c1:symbol) +.decl state_type_mixed(c0:symbol) +.decl state_type_ok(c0:symbol) +.decl state_entry(c0:symbol, c1:symbol) +.decl state_entry_alloc(c0:symbol, c1:symbol) +.decl state_entry_open(c0:symbol) +.decl state_carry(c0:symbol, c1:symbol, c2:symbol, c3:number) +.decl state_enters(c0:symbol, c1:symbol, c2:symbol) +.decl func_written_onto(c0:symbol,c1:symbol,c2:symbol) +.decl state_leak(c0:symbol, c1:symbol) +.decl state_outer_ref(c0:symbol, c1:symbol) +.decl state_outer_carry(c0:symbol, c1:symbol, c2:symbol, c3:number) +.decl state_gated(c0:symbol, c1:symbol) +.decl state_gate_alloc(c0:symbol, c1:symbol, c2:symbol) +.decl state_gate(c0:symbol, c1:symbol, c2:symbol) +.decl state_gated_type(c0:symbol) +.decl state_call_alloc(c0:symbol, c1:symbol, c2:symbol) +.decl state_call_open(c0:symbol, c1:symbol) +.decl state_world_of_gated(c0:symbol, c1:symbol) diff --git a/graph/javascript/souffle/export_manifest.tsv b/graph/javascript/souffle/export_manifest.tsv index 1e8b5aac..d62ccfa3 100644 --- a/graph/javascript/souffle/export_manifest.tsv +++ b/graph/javascript/souffle/export_manifest.tsv @@ -4,6 +4,7 @@ client_calls_client client-to-client-calls.csv client_calls_lib client-to-lib-calls.csv client_calls_lib_detail client-to-lib-navigation.csv unresolved_receiver unresolved-receiver.csv +library_receiver library-receiver.csv method_has_unknown_call method-has-unknown-call.csv unresolved_value_callee unresolved-value-callee.csv entry_point entry-point.csv @@ -42,3 +43,9 @@ package_entry package-entry.csv import_staged_package_unreached import-staged-package-unreached.csv member_write_refused member-write-refused.csv jsx_renders jsx-renders.csv +state_gate state-gate.csv +state_gate_alloc state-gate-alloc.csv +state_call_alloc state-call-alloc.csv +state_call_open state-call-open.csv +state_world_of_gated state-world.csv +field_access field-access.csv diff --git a/graph/pipeline/run-souffle.sh b/graph/pipeline/run-souffle.sh index c26c8774..0347b7a5 100755 --- a/graph/pipeline/run-souffle.sh +++ b/graph/pipeline/run-souffle.sh @@ -34,7 +34,7 @@ JDK_DEPTH=1 # max JDK-hop depth engine-ii expands. FORCED (always applied). De # hop 1; deeper JDK expansion only traces internal plumbing (the explosion source). LIB_DEPTH="" # max external-lib-hop depth. OPTIONAL — empty = UNCAPPED (dive deep into the client's # real deps, no sink catalog there). Set with --lib-depth to bound a runaway library. -DISPATCH_CAP="${DISPATCH_CAP:-20}" # fan-width cap on virtual dispatch. DEFAULT 20 (measured): +DISPATCH_CAP="${DISPATCH_CAP:-}" # fan-width cap on virtual dispatch. DEFAULT 20, off for Python (below) (measured): # on cassandra 6.0-alpha2 it lifts change-impact precision d2 0.782->0.920 and # d3 0.569->0.753 at ZERO recall cost against the must-have bytecode set (recall # 1.000/0.996/0.988 unchanged), and halves fabricated false positives. Cap 10 is @@ -81,6 +81,9 @@ PKG="$(cd "$SRC/.." && pwd)" # the package root: package.json, node_modules, p . "$SRC/pipeline/compile-lock.sh" # Rules are PER-LANGUAGE and live under graph//; the executor itself is shared. LANG_ARG="${LANG_ARG:-java}" +# Python's cap is off by default: a Python site over the cap is dropped outright, and for test selection a missed +# test costs more than an extra one. Every other language keeps 20. An explicit --dispatch-cap still wins. +if [ -z "$DISPATCH_CAP" ]; then [ "$LANG_ARG" = python ] && DISPATCH_CAP=off || DISPATCH_CAP=20; fi ENG="$SRC/$LANG_ARG/engine"; ENG2="$SRC/$LANG_ARG/engine-ii"; DL="$SRC/$LANG_ARG/souffle"; TPL="$SRC/$LANG_ARG/templates" [ -d "$ENG" ] || { echo "no rule set for --language=$LANG_ARG (looked in $ENG)" >&2; exit 1; } # shellcheck source=souffle-include.sh @@ -201,6 +204,7 @@ write_program(){ _SU=() t+="#include \"$LANG_ARG/souffle/decls_base.dl\"$nl#include \"$LANG_ARG/souffle/decls_all.dl\"$nl" map_rels_su "$TPL/client-ir.map" || return 1 + if [ -f "$TPL/client-extra.map" ]; then map_rels_su "$TPL/client-extra.map" || return 1; fi map_rels_su "$TPL/lib.map" sig || return 1 for r in $LIB_BODY; do _SU+=("$r"); done _SU+=(jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world) @@ -254,7 +258,8 @@ write_program(){ # lost on either side shows up as a difference. Every .input and .output line must be exactly # the expected one, each exactly once, with nothing extra. Exit status only: no pipe to lose. verify_program(){ - awk -v cmap="$TPL/client-ir.map" -v lmap="$TPL/lib.map" -v man="$DL/export_manifest.tsv" \ + local xmap=""; [ -f "$TPL/client-extra.map" ] && xmap="$TPL/client-extra.map" + awk -v cmap="$TPL/client-ir.map" -v xmap="$xmap" -v lmap="$TPL/lib.map" -v man="$DL/export_manifest.tsv" \ -v libsig=" $LIB_SIG " -v extra="$LIB_BODY jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world" ' function want(line) { if (!(line in need)) { need[line] = 1; n++ } } function inp(r) { want(".input " r "(IO=file, filename=\"" r ".facts\", delimiter=\"\\t\", rfc4180=true)") } @@ -269,7 +274,7 @@ verify_program(){ close(path) } BEGIN { - maprels(cmap, 0); maprels(lmap, 1) + maprels(cmap, 0); maprels(lmap, 1); if (xmap != "") maprels(xmap, 0) m = split(extra, e, " "); for (i = 1; i <= m; i++) if (e[i] != "") inp(e[i]) while ((rc = (getline l < man)) > 0) { sub(/^\t+/, "", l); k = index(l, "\t"); p = l; f = "" @@ -318,14 +323,14 @@ engine_id_of(){ done < "$prog" hin="$(mktemp "${TMPDIR:-/tmp}/axiom-engine-id.XXXXXX")" && [ -f "$hin" ] \ || { echo "❌ engine id: mktemp failed" >&2; return 1; } - if ! printf 'souffle=%s\n' "$SOUFFLE_VERSION" > "$hin" || ! cat "$prog" ${incs[@]+"${incs[@]}"} >> "$hin"; then + if ! printf 'souffle=%s+seqlock-fix-4\n' "$SOUFFLE_VERSION" > "$hin" || ! cat "$prog" ${incs[@]+"${incs[@]}"} >> "$hin"; then echo "❌ engine id: writing the hash input failed" >&2; rm -f "$hin"; return 1 fi # The expected size comes from the SOURCE files (wc's last line is their total), not from a # second read through cat, so a cat that loses bytes cannot agree with itself. want="$(wc -c "$prog" ${incs[@]+"${incs[@]}"})"; have="$(wc -c < "$hin")" want="${want##*$'\n'}"; want="${want#"${want%%[![:space:]]*}"}"; want="${want%% *}" - case "$want" in ""|*[!0-9]*) want=-1;; *) want=$(( want + ${#SOUFFLE_VERSION} + 9 ));; esac # 9 = "souffle=" + "\n" + case "$want" in ""|*[!0-9]*) want=-1;; *) want=$(( want + ${#SOUFFLE_VERSION} + 23 ));; esac # 23 = "souffle=" + "+seqlock-fix-4" + "\n" if [ "${have//[[:space:]]/}" != "$want" ]; then echo "❌ engine id: the hash input is ${have//[[:space:]]/} bytes, expected $want (short write)" >&2; rm -f "$hin"; return 1 fi @@ -379,8 +384,270 @@ case "${AXIOM_ENGINE_MARCH:-native}" in portable) MARCH_FLAG=();; *) MARCH_FLAG=("-march=${AXIOM_ENGINE_MARCH:-native}");; esac +# ── PARALLEL SOLVE ──────────────────────────────────────────────────────────── +# Soufflé emits parallel loops only when the program is GENERATED with -j — without it +# the C++ holds zero parallel sections, which is why every engine before this was +# sequential by construction. Compiled WITHOUT OpenMP the same generated code runs +# serially (pfor degrades to for), so generation always asks for the parallel loops and +# OpenMP at COMPILE time decides the flavor. Measured on a 6,139-file Java subject: +# 147s serial -> 59s at -j8; the outputs are equal as sets (row order shifts between +# flavors; the bundle loads rows into sqlite, which keeps no order). +# GATED PER PLATFORM. darwin-arm64's nondeterministic parallel segfaults — a different +# rule each crash, g++/libgomp and apple-clang/libomp alike — were two weak-ordering +# holes, both closed by the header overlay below: the write-entry RMW (seqlock-fix-1) +# and the unfenced publication of freshly split btree nodes (seqlock-fix-2), which is +# why the crashes needed memory pressure (a recycled page holds garbage where a fresh +# one holds zeros). darwin-arm64 now runs parallel by default, like linux-x64; Linux +# enables OpenMP when its toolchain takes -fopenmp. AXIOM_SOLVE_PARALLEL=0 forces serial +# anywhere; =1 forces the attempt anywhere (still needs a toolchain with -fopenmp). +# The flavor is part of the CACHE NAME, never shared between flavors: the two binaries +# answer with different row orders, and a cache hit must reproduce the flavor that ran +# yesterday, not whichever compiled first. +# THE SEQLOCK FIX (overlay). Soufflé's OptimisticReadWriteLock enters its write phase +# with fetch_or(..., memory_order_acquire): the version-odd store may become visible +# AFTER the write section's data stores on a weakly-ordered CPU, so a reader can read a +# half-mutated node and still pass validate() against the stale even version. On x86's +# TSO stores never reorder, which is why this only ever fired on arm64 (nondeterministic +# segfaults in a different rule each run, any toolchain, Soufflé 2.5 and master alike). +# seq_cst on the entry RMW pins the odd version BEFORE any data store; on x86 a locked +# RMW is already a full barrier, so the change costs nothing there. +# THE PUBLICATION FIX (fix-2, BTree.h). The residual arm64 crash under memory pressure: +# a split publishes a freshly built node with PLAIN pointer stores — insert_inner's +# `getChildren()[pos + 1] = newNode` and grow_parent's `*root = new_root` — and on a +# weakly-ordered CPU that pointer can become visible BEFORE the node's own field stores, +# including its lock's start_write. An optimistic reader then descends into memory whose +# lock reads as free and whose children are whatever the allocator left there: zeros on a +# fresh page (survivable), garbage on recycled memory — which is exactly the observed +# "under memory pressure, a different rule each run" (TSAN: 66 atomic-vs-plain write pairs, +# all BTree.h insert/split against a node's lock word). A release fence before each +# publication orders every initialization store first; the reader's dereference is +# address-dependent, which arm64 orders by itself. The engine id carries +# "+seqlock-fix-4", so no unpatched cache entry or package is ever taken for a patched one. +souffle_overlay(){ + local inner="$1" overlay="$CACHE_ROOT/include-seqlock-fix-4" + local hdr="$overlay/souffle/utility/ParallelUtil.h" + if [ ! -f "$hdr" ]; then + rm -rf "$overlay.tmp.$$" + mkdir -p "$overlay.tmp.$$" + cp -R "$inner/." "$overlay.tmp.$$/" || return 1 + local h="$overlay.tmp.$$/souffle/utility/ParallelUtil.h" + [ -f "$h" ] || return 1 + # three write-entry RMWs: start_write (two), try_start_write, try_upgrade_to_write + sed -i.bak 's/version\.fetch_or(0x1, std::memory_order_acquire)/version.fetch_or(0x1, std::memory_order_seq_cst)/g' "$h" && rm -f "$h.bak" + grep -q 'fetch_or(0x1, std::memory_order_seq_cst)' "$h" || return 1 + grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$h" && return 1 + # fix-3: a release fence before EVERY store that makes a new node reachable — the child + # links fix-2 fenced, and the parent-pointer publications it missed (split's reparenting, + # grow_parent's this->parent/sibling->parent, insert_inner's late newNode->parent, which + # is reordered to before the link). The lock-parents walk reads exactly those pointers. + # ...and the MINGW-COMPILE guards: souffle maps __builtin_popcountll to MSVC's + # __popcnt64 and pulls intrin.h for ALL of _WIN32, which breaks g++ on MinGW — the + # one toolchain a Windows developer who clones this repository compiles with. Both + # are MSVC-only concerns, so the guards narrow to _MSC_VER; every other platform's + # preprocessed output is bit-identical. + local b="$overlay.tmp.$$/souffle/datastructure/BTree.h" + [ -f "$b" ] || return 1 + python3 - "$b" <<'PYEOF' || return 1 +import glob, os, sys +p = sys.argv[1]; s = open(p).read() +root = os.path.dirname(os.path.dirname(p)) +F = "std::atomic_thread_fence(std::memory_order_release); // publication fence (seqlock-fix-3)" +# split(): fill the sibling completely (children, counts), fence, THEN reparent — each +# children[j]->parent = other makes the new sibling reachable from an existing node. +a1 = """ // move child pointers + if (this->inner) { + // move pointers to sibling + auto* other = static_cast(sibling); + for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) { + other->children[j] = getChildren()[i]; + other->children[j]->parent = other; + other->children[j]->position = static_cast(j); + } + } + + // update number of elements + this->numElements = split_point; + sibling->numElements = maxKeys - split_point - 1;""" +r1 = """ // move child pointers: the sibling's own fields first (private until reparented) + if (this->inner) { + auto* other = static_cast(sibling); + for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) { + other->children[j] = getChildren()[i]; + } + } + sibling->numElements = maxKeys - split_point - 1; + """ + F + """ + if (this->inner) { + auto* other = static_cast(sibling); + for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) { + other->children[j]->parent = other; + other->children[j]->position = static_cast(j); + } + } + + // update number of elements + this->numElements = split_point;""" +# grow_parent(): new_root is fully filled above; the parent stores are its first publication. +a2 = """ // link this and the sibling node to new root + this->parent = new_root;""" +r2 = """ // link this and the sibling node to new root + """ + F + """ + this->parent = new_root;""" +a3 = " // switch root node\n *root = new_root;" +r3 = " // switch root node\n " + F + "\n *root = new_root;" +# insert_inner(): give newNode its parent and position BEFORE the link that publishes it. +a4 = """ keys[pos] = key; + getChildren()[pos + 1] = newNode; + newNode->parent = this; + newNode->position = static_cast(pos) + 1;""" +r4 = """ keys[pos] = key; + newNode->parent = this; + newNode->position = static_cast(pos) + 1; + """ + F + """ + getChildren()[pos + 1] = newNode;""" +# spec-descent-guard (seqlock-fix-4): the insert descent dereferences the child it read +# under a still-unvalidated optimistic lease; a concurrent split exposes a null slot. +# CAPTURED on darwin-arm64: ldapr of the child's seqlock version at address null+8, +# inside btree::insert, java -j8 under souffle-g memory load. A null child is a failed +# validation and restarts the insert -- never a dereference. Applied to BTree.h and +# its BTreeDelete.h twin. +d = """ // get next pointer + auto next = cur->getChild(idx); + + // get lease on next level + auto next_lease = next->lock.start_read();""" +dr = """ // get next pointer + auto next = cur->getChild(idx); + + // spec-descent-guard (seqlock-fix-4): a concurrent split can expose a + // null child slot under an optimistic lease; treat it as a failed + // validation, never dereference it. + if (next == nullptr) { + return insert(k, hints); + } + + // get lease on next level + auto next_lease = next->lock.start_read();""" +if s.count(d) != 1: sys.exit(1) +s = s.replace(d, dr) +bd = os.path.join(os.path.dirname(p), "BTreeDelete.h") +t = open(bd).read() +if t.count(d) != 1: sys.exit(1) +open(bd, "w").write(t.replace(d, dr)) +for a, r in ((a1, r1), (a2, r2), (a3, r3), (a4, r4)): + if s.count(a) != 1: sys.exit(1) + s = s.replace(a, r) +open(p, 'w').write(s) +# mingw-compile guards, in every copy the include layout holds +m1 = "#if _WIN64\n#define __builtin_popcountll __popcnt64\n#else\n#define __builtin_popcountll __popcnt\n#endif" +m1r = "#if defined(_MSC_VER)\n" + m1 + "\n#endif // _MSC_VER (mingw-compile): MinGW has the gcc builtins" +m2 = "#ifdef _WIN32\n#include " +m2r = "#if defined(_MSC_VER) // (mingw-compile): intrin.h only for MSVC\n#include " +hit1 = hit2 = 0 +for q in glob.glob(root + "/**/MiscUtil.h", recursive=True) + glob.glob(root + "/**/PiggyList.h", recursive=True): + t = open(q).read(); n = 0 + if m1 in t: t = t.replace(m1, m1r); hit1 += 1; n = 1 + if m2 in t: t = t.replace(m2, m2r); hit2 += 1; n = 1 + if n: open(q, "w").write(t) +if hit1 < 1 or hit2 < 1: sys.exit(1) +# spec-fetch-guard (seqlock-fix-4): an optimistic BTree reader may hand the flyweight a +# SPECULATIVELY-READ index before validating its seqlock. A torn or stale index must come +# back as a harmless empty value that the validation then discards -- never a dereference +# of an unassigned (null) or out-of-range slot. Observed as a segfault in a varying rule, +# javascript -j8 on darwin-arm64 under memory-pressure load (4 hits in two runs). +fw = glob.glob(root + "/**/ConcurrentFlyweight.h", recursive=True)[0] +t = open(fw).read() +fa = """ const Key& fetch(const lane_id H, const index_type Idx) const { + const auto Lane = Lanes.guard(H); + assert(Idx < SlotCount.load(std::memory_order_relaxed)); + return Slots[Idx]->first; + }""" +fr = """ const Key& fetch(const lane_id H, const index_type Idx) const { + const auto Lane = Lanes.guard(H); + // spec-fetch-guard (seqlock-fix-4): tolerate a speculative index, see overlay notes. + if (Idx >= SlotCount.load(std::memory_order_relaxed) || Slots[Idx] == nullptr) { + static const Key Empty{}; + return Empty; + } + return Slots[Idx]->first; + }""" +if t.count(fa) != 1: sys.exit(1) +open(fw, "w").write(t.replace(fa, fr)) +rt = glob.glob(root + "/**/RecordTableImpl.h", recursive=True)[0] +t = open(rt).read() +ua = """ const RamDomain* unpack(RamDomain Index) const override { + return fetch(Index).data(); + }""" +ur = """ const RamDomain* unpack(RamDomain Index) const override { + // spec-fetch-guard (seqlock-fix-4): an empty generic record has a null data(); + // give a speculative reader zeroed storage to misread instead. + const auto& R = fetch(Index); + if (R.data() == nullptr) { + static const RamDomain Zeros[64] = {}; + return Zeros; + } + return R.data(); + }""" +if t.count(ua) != 1: sys.exit(1) +open(rt, "w").write(t.replace(ua, ur)) +PYEOF + [ "$(grep -c 'seqlock-fix-3' "$b")" = "4" ] || return 1 + [ "$(grep -c 'seqlock-fix-4' "$b")" = "1" ] || return 1 + [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/BTreeDelete.h")" = "1" ] || return 1 + [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/ConcurrentFlyweight.h")" = "1" ] || return 1 + [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/RecordTableImpl.h")" = "1" ] || return 1 + mv "$overlay.tmp.$$" "$overlay" 2>/dev/null || true # a concurrent run may have won; theirs is identical + rm -rf "$overlay.tmp.$$" + fi + [ -f "$hdr" ] && printf '%s' "$overlay" +} +OMP_FLAG=(); PAR_SUFFIX="" +probe_openmp(){ + # the flags this platform needs for a working OpenMP compile, or nothing. + # Linux: -fopenmp everywhere. Darwin: Apple clang only lowers the pragmas with the + # frontend flag plus Homebrew's libomp (-Xpreprocessor defines _OPENMP without + # lowering anything — a silently sequential binary, which is how this stayed hidden). + case "$(uname -s)" in + Linux|MINGW*|MSYS*|CYGWIN*) + # MSYS2/Git-Bash `c++` is g++, which takes -fopenmp exactly as Linux does + if printf 'int main(){return 0;}' | c++ -fopenmp -x c++ -o /dev/null - 2>/dev/null; then + printf '%s' "-fopenmp"; return 0 + fi ;; + Darwin) + local omp + for omp in /opt/homebrew/opt/libomp /usr/local/opt/libomp; do + [ -f "$omp/lib/libomp.dylib" ] || continue + if printf 'int main(){return 0;}' | c++ -Xclang -fopenmp -I "$omp/include" -L "$omp/lib" -lomp -x c++ -o /dev/null - 2>/dev/null; then + printf '%s' "-Xclang -fopenmp -I $omp/include -L $omp/lib -lomp"; return 0 + fi + done ;; + esac + return 1 +} +# Default: Linux x86_64 only. The seqlock entry fix above repairs the diagnosed +# ordering hole (quiet-machine runs went clean), but a residual crash mode remains on +# arm64 under memory pressure, so weakly-ordered CPUs stay serial by default until it +# is found; AXIOM_SOLVE_PARALLEL=1 opts any machine in for experiments. +case "${AXIOM_SOLVE_PARALLEL:-}" in + 0) ;; + 1) if _OMP="$(probe_openmp)"; then + OMP_FLAG=($_OMP); PAR_SUFFIX="-par" + fi ;; + "") # Parallel by default on every platform with evidence behind it. The two arm64 + # weak-ordering holes (write-entry RMW; unfenced node publication) are closed by + # the seqlock-fix-3 overlay, validated under load: darwin-arm64 10/10, linux-arm64 + # 10/10 (GCP), linux-x64 5/5 (GCP) — all relation-identical to serial. x64 is TSO, + # where neither hole is observable. Windows local compiles probe like the rest + # (MSYS g++); packaged win32 engines carry /openmp from build-engines.yml, gated + # by its own release validation. AXIOM_SOLVE_PARALLEL=0 is the one-variable + # rollback to serial anywhere, no rebuild. + if _OMP="$(probe_openmp)"; then + # shellcheck disable=SC2206 — the probe emits simple flags, split wanted + OMP_FLAG=($_OMP); PAR_SUFFIX="-par" + fi ;; +esac EXE=""; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) EXE=".exe";; esac -BIN="$CACHE_DIR/souffle-engine-$LANG_ARG-$ENGINE_ID$EXE" +BIN="$CACHE_DIR/souffle-engine-$LANG_ARG-$ENGINE_ID$PAR_SUFFIX$EXE" # The platform string, in npm's spelling (process.platform-process.arch), because that is # how the engine packages are named: darwin-arm64, linux-x64, linux-arm64, win32-x64. @@ -456,14 +723,16 @@ elif [ -z "$PACKAGED" ] && [ -n "${COMPILE_LOCK:-}" ]; then # line blocks from stderr; on a real failure, dump the full log and fail. c++ -w # silences the deprecation warnings in souffle's own headers. Compile to a .tmp then # atomically rename, so a concurrent/aborted run never leaves a half-written binary. - if ! souffle -I "$SRC" -g "$INT/souffle-program.cpp" "$PROG" 2> "$INT/.souffle-gen.log"; then + if ! souffle -I "$SRC" -j 8 -g "$INT/souffle-program.cpp" "$PROG" 2> "$INT/.souffle-gen.log"; then cat "$INT/.souffle-gen.log" >&2; exit 1 fi awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "$INT/.souffle-gen.log" >&2 [ -s "$INT/souffle-program.cpp" ] || { echo "❌ souffle wrote no C++ for $PROG" >&2; exit 1; } CXX_PLATFORM="" case "$(uname -s)" in CYGWIN*) CXX_PLATFORM="-Wa,-mbig-obj";; esac - if ! c++ -std=c++17 -O3 ${MARCH_FLAG[@]+"${MARCH_FLAG[@]}"} -w $CXX_PLATFORM -I "$INNER" "$INT/souffle-program.cpp" -o "$BIN.tmp.$$"; then + OVERLAY="$(souffle_overlay "$INNER" || true)" + [ -n "$OVERLAY" ] || { echo "❌ could not prepare the patched soufflé headers (seqlock fix)" >&2; exit 1; } + if ! c++ -std=c++17 -O3 ${MARCH_FLAG[@]+"${MARCH_FLAG[@]}"} ${OMP_FLAG[@]+"${OMP_FLAG[@]}"} -w $CXX_PLATFORM -I "$OVERLAY" -I "$INNER" "$INT/souffle-program.cpp" -o "$BIN.tmp.$$"; then rm -f "$BIN.tmp.$$"; echo "❌ compiling the engine failed" >&2; exit 1 fi # VERIFY, THEN PUBLISH. The cache entry is trusted by name alone from now on, so nothing may @@ -549,7 +818,7 @@ while IFS=$'\t' read -r rel csv; do if [ -f "$CLIENT/$csv.csv" ]; then awk -F'\t' 'NR==1{n=NF; next} NF==n{print; next} {bad++} END{if(bad>0) printf " ! dropped %d malformed row(s) from %s\n", bad, FILENAME > "/dev/stderr"}' "$CLIENT/$csv.csv" > "$FACTS/$rel.facts" else : > "$FACTS/$rel.facts"; fi CLIENT_INPUTS="$CLIENT_INPUTS$rel"$'\n' -done < <(read_map "$TPL/client-ir.map") +done < <(read_map "$TPL/client-ir.map"; if [ -f "$TPL/client-extra.map" ]; then read_map "$TPL/client-extra.map"; fi) # --- LIB: signature relations, concatenated across every module of every root --- # CACHED. This concatenation reads the ENTIRE library IR (the JDK alone is 2.0 GB in, @@ -771,7 +1040,22 @@ while [ "$iter" -lt 50 ]; do # the instructions it uses (a shared cache, a CI cache keyed too coarsely), it dies # with SIGILL (exit 132) before solving anything. Never leave it there to kill every # later run the same way: drop the cache entry, so the next run recompiles, and say so. - rc=0; "$BIN" -F "$FACTS" -D "$RAW" || rc=$? + # -j is passed ALWAYS (a serial binary ignores it silently — verified); more than one + # thread only for a binary of the parallel flavor: one this run compiled with OpenMP, + # or a packaged/cached one whose builder left a .parallel marker beside it. + SOLVE_J=1 + if [ -n "$PAR_SUFFIX" ] || [ -f "$BIN.parallel" ]; then + cores="$( (command -v nproc >/dev/null 2>&1 && nproc) || sysctl -n hw.ncpu 2>/dev/null || echo 4 )" + SOLVE_J="${AXIOMCODE_SOLVE_THREADS:-$(( cores < 8 ? cores : 8 ))}" + fi + # AXIOM_DL_PROFILE=: solve this run in the souffle INTERPRETER with per-rule profiling + # written to (souffleprof reads it). Slower than the compiled engine, same answer; for + # finding the rules a solve spends its time in, as the java local-use hoist was found. + if [ -n "${AXIOM_DL_PROFILE:-}" ] && command -v souffle >/dev/null 2>&1; then + rc=0; souffle -I "$SRC" -j "$SOLVE_J" -p "$AXIOM_DL_PROFILE" -F "$FACTS" -D "$RAW" "$PROG" || rc=$? + else + rc=0; "$BIN" -j "$SOLVE_J" -F "$FACTS" -D "$RAW" || rc=$? + fi if [ "$rc" -ne 0 ]; then if [ "$rc" -eq 132 ] && [ -z "$PACKAGED" ]; then rm -f "$BIN" @@ -810,7 +1094,7 @@ done # here (it's in the shared cache), and facts must stay per-run (never shared) so concurrent # analyses of different projects don't collide. Runs only on success (set -e bails earlier # on failure, leaving the facts for debugging). -rm -rf "$FACTS" "$INT/souffle-program.cpp" +[ "${AXIOM_KEEP_FACTS:-0}" = "1" ] || rm -rf "$FACTS" "$INT/souffle-program.cpp" SOLVE_EPOCH=$(date +%s) echo "Elapsed (solve): $((SOLVE_EPOCH-START_EPOCH))s" diff --git a/graph/python/engine/call-edge-generation/call_chain.dl b/graph/python/engine/call-edge-generation/call_chain.dl index 622c5a81..ade21045 100644 --- a/graph/python/engine/call-edge-generation/call_chain.dl +++ b/graph/python/engine/call-edge-generation/call_chain.dl @@ -156,6 +156,40 @@ method_returns_method("client", m, r) :- expr_call_candidate(site, g), method_returns_method("client", g, r). +// A function returning a member it LOOKED UP BY NAME: `def get_visitor(self, node): return +// getattr(self, f"visit_{type(node).__name__}", None)`, called as `f = self.get_visitor(node); +// f(node)`. The lookup is resolved where it is written (name-resolution.dl's getattr clauses), +// and the method hands that set back exactly as it would hand back a bare name. +method_returns_method("client", m, r) :- + method_return_value_expr("client", m, e), + call_of_expr(e, site), call_name(site, "getattr"), + expr_denotes_method("client", e, r). + +// ── a call that hands back one of its arguments (py_returns_arg, resolution/builtins.dl) ── +// Matched through the import that binds the callee, never by the bare name: +// `functools.update_wrapper(...)` / `t.cast(...)` where the receiver is the name an `import` +// bound, or `update_wrapper(...)` bound by `from functools import update_wrapper`. A local +// function that happens to share the name does neither. +py_passthrough_arg(site, pos) :- + call_name(site, fn), call_receiver_object(site, obj), + expr_binding("client", rb, ctx, obj), ctx != "STORE", binding_lookup("client", rb, rb2), + import_binding("client", rb2, i), import_decl("client", k, mod, _, i), + (k = "MODULE_IMPORT" ; k = "MODULE_IMPORT_ALIAS"), // `import typing as t` is the alias kind + py_returns_arg(path, pos), cat(mod, cat(".", fn)) = path. +py_passthrough_arg(site, pos) :- + call_callee_is_value(site), call_callee_expr(site, callee), + expr_binding("client", rb, ctx, callee), ctx != "STORE", binding_lookup("client", rb, rb2), + import_binding("client", rb2, i), import_decl("client", _, path, _, i), + py_returns_arg(path, pos). +// the expression a value really is, through any number of such calls +py_passthrough_root(e, e) :- method_return_value_expr("client", _, e). +py_passthrough_root(e, x) :- + py_passthrough_root(e, c), call_of_expr(c, site), py_passthrough_arg(site, pos), + call_arg(site, pos, x). +method_returns_method("client", m, r) :- + method_return_value_expr("client", m, e), py_passthrough_root(e, x), x != e, + expr_names_method("client", x, r). + // ── decorator_hits_lib(SiteKey, LibMethodHash) ─────────────────────────────── // `@abstractmethod` names `abc.abstractmethod`, which HAS Python source and is in the // staged stdlib IR — it is a library boundary, not a blind spot. A BARE decorator has no @@ -320,6 +354,25 @@ iter_protocol_edge(src, caller, m) :- expr_type("client", src, t), iter_protocol_target(t, m), expr_ultimate_method("client", src, caller). +// ── property_write_edge(WriteExprHash, CallerMethodHash, AccessorMethodHash) ── +// The store and delete halves of the property protocol: `obj.x = v` calls x's setter, +// `del obj.x` its deleter (type_property_accessor, resolution/attribute-lookup.dl). Same +// shape as a read, keyed on the access's own name context. +property_write_edge(e, caller, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), + expr_name_context("client", "STORE", e), + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), + expr_type("client", obj, t), + type_property_accessor("client", t, n, "PROPERTY_SETTER", m), + expr_ultimate_method("client", e, caller). +property_write_edge(e, caller, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), + expr_name_context("client", "DEL", e), + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), + expr_type("client", obj, t), + type_property_accessor("client", t, n, "PROPERTY_DELETER", m), + expr_ultimate_method("client", e, caller). + // ── property_read_edge(ReadExprHash, CallerMethodHash, GetterMethodHash) ───── property_read_edge(e, caller, getter) :- expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), @@ -426,9 +479,12 @@ site_class(site, "ambiguous_unknown") :- // WHY a site is unresolved. This is the difference between a report and a shrug, and it // is what makes an ambiguous_unknown auditable rather than a dumping ground. site_reason(site, r) :- call_unresolvable(site, r), site_class(site, "ambiguous_unknown"). +// decorator_expr first: it binds d from site, so the annotation probe is keyed — written +// annotation-first the planner crossed every annotation with every ambiguous site (25s +// on a 526k-LOC subject for a diagnostic label). site_reason(site, "unmodelled_decorator") :- site_class(site, "ambiguous_unknown"), - annotation_on("client", _, _, _, _, d), decorator_expr("client", site, d). + decorator_expr("client", site, d), annotation_on("client", _, _, _, _, d). // The APPLICATION of a parenthesised decorator whose factory return the engine cannot // type — a library factory such as functools.wraps. Named, not shrugged at. site_reason(site, "decorator_factory_result_untyped") :- @@ -626,6 +682,99 @@ subscript_protocol_edge(sub, caller, m) :- mro_lookup("client", t, "__class_getitem__", m), expr_ultimate_method("client", sub, caller). +// ── BUILTIN PROTOCOL — `repr(x)` runs type(x).__repr__ ───────────────────────── +// A builtin function that exists to call one dunder of its argument: repr, str, len, +// hash, bool, iter, next, abs, format, reversed. The written call resolves to the builtin +// (C code, boundary_lib), and the dunder it runs -- a client method, often the one a test +// exists to check -- had no caller: `assert repr(v) == "<...>"` reached nothing of v's. +// Only the BARE builtin name (bound to nothing in the client, so it is the builtin), with +// exactly one argument (`str(b, "utf-8")` decodes and runs no __str__), on an argument the +// engine can type. str() falls back to __repr__ and bool() to __len__ when the class +// defines no __str__ / __bool__ of its own, as CPython's type slots do. The builtin names +// and their dunders are catalogued in resolution/builtins.dl (builtin_protocol_slot). +builtin_protocol_arg(e, fn, x) :- + call_of_expr(e, site), call_callee_is_value(site), call_callee_expr(site, callee), + expr_binding("client", b, _, callee), binding_lookup_unresolved("client", b, fn, _), + builtin_protocol_slot(fn, _), + call_arg(site, "0", x), !call_arg(site, "1", _), !call_kwarg(site, _, _). +// The edge's site is the ARGUMENT expression, as an iteration's is the iterated one: the +// call expression is already the written call to the builtin. +builtin_protocol_edge(x, caller, m) :- + builtin_protocol_arg(_, fn, x), builtin_protocol_slot(fn, d), + expr_type("client", x, t), mro_lookup("client", t, d, m), + expr_ultimate_method("client", x, caller). +builtin_protocol_edge(x, caller, m) :- + builtin_protocol_arg(_, fn, x), builtin_protocol_fallback(fn, d, d2), + expr_type("client", x, t), !type_has_member(t, d), mro_lookup("client", t, d2, m), + expr_ultimate_method("client", x, caller). +type_has_member(t, d) :- builtin_protocol_fallback(_, d, _), mro_lookup("client", t, d, _). + +// ── OPERATOR PROTOCOL — `a + b`, `a == b`, `x in c`, `-a` run a dunder ──────── +// An operator compiles to BINARY_OP / COMPARE_OP / CONTAINS_OP / UNARY_*, never to a CALL, +// so there is no written site, and the dunder it runs -- a client `__eq__`, `__or__`, +// `__contains__` -- had no caller at all: a test asserting `a == b` or composing two +// strategies with `|` reached nothing of theirs. Same channel as the protocol edges above, +// its own kind. The site is the operator expression. Both halves of the protocol are +// emitted, because both can run: the left operand's slot, and the right operand's +// reflected one when the left is missing it or returns NotImplemented (a comparison's +// mirror, `a < b` -> b.__gt__). The operators and their dunders are catalogued in +// resolution/builtins.dl. A chained comparison (`a < b < c`) carries several operators in +// one node and matches no catalogue row, so it emits nothing rather than a guess. +operator_protocol_edge(e, caller, m) :- + expr_operator("client", op, e), operator_protocol_slot(op, d, _), + expr_parent("client", e, "OPERAND_LEFT", _, l), expr_type("client", l, t), + mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller). +operator_protocol_edge(e, caller, m) :- + expr_operator("client", op, e), operator_protocol_slot(op, _, d), + expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t), + mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller). +// `a != b` on a class with no __ne__ of its own runs __eq__ through object.__ne__. +operator_protocol_edge(e, caller, m) :- + expr_operator("client", op, e), operator_protocol_slot(op, d, _), operator_protocol_fallback(d, d2), + expr_parent("client", e, "OPERAND_LEFT", _, l), expr_type("client", l, t), + !operator_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", e, caller). +operator_type_has(t, d) :- operator_protocol_fallback(d, _), mro_lookup("client", t, d, _). +// `x in c` runs the RIGHT operand's __contains__, or iterates it when it has none. +operator_protocol_edge(e, caller, m) :- + expr_operator("client", op, e), membership_operator(op), membership_protocol_slot(d), + expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t), + mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller). +operator_protocol_edge(e, caller, m) :- + expr_operator("client", op, e), membership_operator(op), membership_protocol_fallback(d, d2), + expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t), + !membership_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", e, caller). +membership_type_has(t, d) :- membership_protocol_fallback(d, _), mro_lookup("client", t, d, _). +// `-a`, `+a`, `~a` +operator_protocol_edge(e, caller, m) :- + expr_node("client", "UNARY_OPERATION", _, _, e), expr_operator("client", op, e), unary_protocol_slot(op, d), + expr_parent("client", e, "UNARY_OPERAND", _, x), expr_type("client", x, t), + mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller). + +// ── TRUTH PROTOCOL — `if x:` runs type(x).__bool__, or __len__ ─────────────── +// Truth testing has no written call either: `if x:`, `while x:`, `assert x`, `not x`, the +// condition of `a if x else b` and of a comprehension, and an operand of `and` / `or` all +// run x's __bool__ -- or __len__ when the class defines no __bool__ (data model, +// object.__bool__). The left operand of `and`/`or` is always tested; the right one only +// when the whole expression is (`return a or b` hands b back untested). The site is the +// tested expression. +truth_tested(x) :- + truth_test_root(rc), expr_root_context("client", rc, x), !expr_has_parent("client", x). +truth_tested(x) :- truth_test_role(role), expr_parent("client", _, role, _, x). +truth_tested(x) :- + expr_node("client", "UNARY_OPERATION", _, _, e), expr_operator("client", op, e), truth_negation_operator(op), + expr_parent("client", e, "UNARY_OPERAND", _, x). +truth_tested(x) :- expr_node("client", "BOOLEAN_OPERATION", _, _, e), expr_parent("client", e, "OPERAND_LEFT", _, x). +truth_tested(x) :- + expr_node("client", "BOOLEAN_OPERATION", _, _, e), truth_tested(e), expr_parent("client", e, "OPERAND_RIGHT", _, x). + +truth_protocol_edge(x, caller, m) :- + truth_tested(x), truth_protocol_fallback(d, _), expr_type("client", x, t), + mro_lookup("client", t, d, m), expr_ultimate_method("client", x, caller). +truth_protocol_edge(x, caller, m) :- + truth_tested(x), truth_protocol_fallback(d, d2), expr_type("client", x, t), + !truth_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", x, caller). +truth_type_has(t, d) :- truth_protocol_fallback(d, _), mro_lookup("client", t, d, _). + // ── THE THREE PROTOCOL EDGES ARE TIERED BY TARGET COUNT, like every other edge ── // // A property read, a context-manager entry and an iteration all reach their target @@ -665,9 +814,13 @@ subscript_protocol_edge(sub, caller, m) :- // from the graph rather than being re-tiered. A dropped edge is worse than a mislabelled // one, and the golden caught it. protocol_edge(e, d, caller, m) :- property_read_edge(e, caller, m), method_decl(_, d, _, _, _, m). +protocol_edge(e, d, caller, m) :- property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- with_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- iter_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). protocol_edge(e, d, caller, m) :- subscript_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). +protocol_edge(e, d, caller, m) :- builtin_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). +protocol_edge(e, d, caller, m) :- operator_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). +protocol_edge(e, d, caller, m) :- truth_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m). // Aggregate in its own stratum, mirroring site_client_target_count above. protocol_edge_target_count(e, d, n) :- @@ -728,6 +881,12 @@ call_chain_edge(e, caller, "-", getter, "client", cls, "PROPERTY_READ") :- property_read_edge(e, caller, getter), method_decl(_, d, _, _, _, getter), protocol_edge_class(e, d, cls). +// PROPERTY WRITE — `obj.x = v` runs x's setter and `del obj.x` its deleter. Likewise no +// call site, and its own kind so it is never counted as a written call. +call_chain_edge(e, caller, "-", m, "client", cls, "PROPERTY_WRITE") :- + property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m), + protocol_edge_class(e, d, cls). + // CONTEXT MANAGER — the same shape: an edge with no call site, its own kind so it can // never be mistaken for a written call. call_chain_edge(cm, caller, "-", m, "client", cls, "CONTEXT_MANAGER") :- @@ -746,6 +905,22 @@ call_chain_edge(sub, caller, "-", m, "client", cls, "SUBSCRIPT_PROTOCOL") :- subscript_protocol_edge(sub, caller, m), method_decl(_, d, _, _, _, m), protocol_edge_class(sub, d, cls). +// BUILTIN PROTOCOL — likewise. Its own kind: the written call is to the builtin, and +// this edge is the dunder that builtin runs, never a second written call. +call_chain_edge(x, caller, "-", m, "client", cls, "BUILTIN_PROTOCOL") :- + builtin_protocol_edge(x, caller, m), method_decl(_, d, _, _, _, m), + protocol_edge_class(x, d, cls). + +// OPERATOR PROTOCOL — an operator's dunder. Its own kind: there is no written call. +call_chain_edge(e, caller, "-", m, "client", cls, "OPERATOR_PROTOCOL") :- + operator_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m), + protocol_edge_class(e, d, cls). + +// TRUTH PROTOCOL — the __bool__ (or __len__) a truth test runs. Its own kind likewise. +call_chain_edge(x, caller, "-", m, "client", cls, "TRUTH_PROTOCOL") :- + truth_protocol_edge(x, caller, m), method_decl(_, d, _, _, _, m), + protocol_edge_class(x, d, cls). + // A site whose caller could not be determined AT ALL would vanish from every rule // above. That must be impossible (the parser guarantees a non-empty owner), so it is // asserted rather than assumed: any such site is emitted with caller "-" so the @@ -819,7 +994,10 @@ method_has_unknown_call(caller, site) :- // ── call_chain_summary(EdgeStatus, N) ──────────────────────────────────────── // The conservation ledger, as a relation: the tier counts must sum to the site count. +// One count per DISTINCT class: driven by site_class rows, the aggregate re-counted the +// class for every site in it (sites x class-size on a large subject, 15s for a ledger). +site_class_kind(cls) :- site_class(_, cls). call_chain_summary(cls, n) :- - site_class(_, cls), + site_class_kind(cls), n = count : { site_class(_, cls) }. call_chain_summary("_total_sites", n) :- n = count : { invocation_site(_, _) }, n > 0. diff --git a/graph/python/engine/config-resolution/knobs.dl b/graph/python/engine/config-resolution/knobs.dl index 19231f11..27c24871 100644 --- a/graph/python/engine/config-resolution/knobs.dl +++ b/graph/python/engine/config-resolution/knobs.dl @@ -165,6 +165,14 @@ py_parametrize_deco("parametrize"). py_parametrize_argnames_kw("argnames"). py_parametrize_indirect_kw("indirect"). py_parametrize_indirect_all("True"). +// The VALUES the runner hands in: the decorator's second argument (or `argvalues=`), a +// fixture's `params=`, a row wrapped as `pytest.param(v, ..., id=...)`, and, inside a fixture +// declared with params, the attribute `request.param` it reads the current one from. +py_parametrize_argvalues_kw("argvalues"). +py_fixture_params_kw("params"). +py_param_row_wrapper("param"). +py_fixture_request_param("request"). +py_request_param_attr("param"). // ── py_argnames_sep / py_argnames_lead: how "a, b" separates argument names ── // The runner splits the string on "," and strips each piece. Souffle cannot split, so a diff --git a/graph/python/engine/expression-resolution/expr-type.dl b/graph/python/engine/expression-resolution/expr-type.dl index 8385f783..ed530619 100644 --- a/graph/python/engine/expression-resolution/expr-type.dl +++ b/graph/python/engine/expression-resolution/expr-type.dl @@ -152,23 +152,30 @@ expr_type(p, e, t) :- // of any length follows by recursion, since the object's type is derived the same way. // Same two field-type sources and the same declared_dispatch widening as the self clauses, // so no new precision assumption is introduced. -expr_type(p, e, t) :- + +// ── attr_access(p, e, n, obj): a named attribute access and its object, ONCE ───────── +// This three-atom prefix sat inline in 13 recursive clauses of the typing fixpoint, and +// semi-naive evaluation re-ran the expr_node scan every iteration — 79 iterations on a +// 526k-LOC subject, ~20% of the solve between them. Hoisted below the fixpoint (pure +// syntax), each clause starts from its recursive delta and probes this table. +attr_access(p, e, n, obj) :- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj). + +expr_type(p, e, t) :- + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_declared_type(p, f, d), declared_dispatch(p, d, t). expr_type(p, e, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_type_from_param(p, f, d), declared_dispatch(p, d, t). expr_type(p, e, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_type_from_construction(p, f, t). @@ -179,8 +186,7 @@ expr_type(p, e, t) :- // an attribute written through `self` in a method does not exist on the class object, // so typing `Cls.x` from it would name something the class never holds. expr_type(p, e, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type_class_object(p, obj, ot), type_attr_field(p, ot, n, f), field_decl(p, n, _, "CLASS_BODY_ASSIGN", f), @@ -212,8 +218,7 @@ expr_type(p, e, t) :- // Same widening as everywhere else: the declared type goes out through declared_dispatch, // so an overriding subclass is not dropped and the annotation is not treated as exact. property_read_of(p, e, obj, n, g) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_property_getter(p, ot, n, g). // A USER-WRITTEN DATA DESCRIPTOR reaches the same two clauses (issue #326). `@property` @@ -221,8 +226,7 @@ property_read_of(p, e, obj, n, g) :- // agree; type_descriptor_getter (resolution/attribute-lookup.dl) is deliberately shaped // like type_property_getter so this is one more clause rather than a parallel path. property_read_of(p, e, obj, n, g) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_descriptor_getter(p, ot, n, g). // (i3a) the getter's declared `-> X`. @@ -430,6 +434,19 @@ binding_value_type(p, b, t) :- assign_pair(p, tgt, val), expr_type(p, val, t). +// ── AN IMPORTED MODULE-LEVEL VALUE IS THE BINDING IT NAMES (#1140) ─────────── +// `from order_service import order_service` resolves to the exporting module's +// own binding row (import_resolved_target kind VARIABLE), and that binding's +// value type is derived above from its assignment. Without this clause the +// type stopped at the module boundary: the same `order_service.cancel()` was a +// known edge next to the assignment and a name match one import away. The +// write-count trade-offs are inherited, not re-decided -- a multi-write export +// arrives as the union and the tier follows from the target count as usual. +binding_value_type(p, b, t) :- + import_binding(p, b, i), + import_resolved_target(p, "VARIABLE", b2, i), + binding_value_type(p, b2, t). + // ── A CONDITIONAL EXPRESSION IS EITHER BRANCH ──────────────────────────────── // `w = HtmlWriter() if flag else PlainWriter()`. CONDITIONAL_EXPRESSION was present in // the IR with BODY / CONDITION / ORELSE children and NO rule read it, so a ternary had @@ -568,6 +585,15 @@ binding_declared_nominal(p, bind, d) :- binding_declared_ref(p, bind, r), type_ref_resolved(p, d, r), !type_is_structural(p, d). +// `x: Optional[Order] = ...` / `x: Order | None = ...` IS an Order (or None) — the rule the +// parameter, return and field annotations already have (resolution/annotations.dl, +// "Optional[X] IS X"), which the local annotation alone was missing: a local written +// with the commonest modern spelling stayed untyped while the same annotation on a +// parameter resolved. +binding_declared_nominal(p, bind, t) :- + type_ref_owner(p, bind, "BINDING", r), + type_ref(p, k, "VARIABLE_ANNOTATION", _, _, r), annotation_optional_kind(k), + type_ref_element(p, r, t), !type_is_structural(p, t). // The FK is resolved on only 14 of 479 variable annotations, so the name-based lookup // beside it is what carries this -- the same two clauses the parameter path uses, with // the same kind restriction. A SUBSCRIPT or a UNION names no single type and is served by @@ -645,8 +671,7 @@ expr_type_untypable(p, e, "unresolved_name") :- // (2b) the same hop through ANY object, not only self. expr_type_untypable(p, e, "attribute_untyped") :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_type_unknown(p, f), !expr_any_type(p, e). @@ -656,8 +681,7 @@ expr_type_untypable(p, e, "attribute_untyped") :- // one wants the field annotated or its writes typed, the other wants the object typed. // Reporting them as one reason would point at the wrong layer. expr_type_untypable(p, e, "attribute_object_untyped") :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), !expr_any_type(p, obj), !expr_any_type(p, e). @@ -674,8 +698,7 @@ expr_type_untypable(p, e, "attribute_object_untyped") :- // `handler` is a METHOD is a call through a bound method, which resolution/value-flow.dl // owns; labelling it "absent" would be wrong rather than merely unhelpful. expr_type_untypable(p, e, "attribute_absent_on_type") :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), !type_attr_field(p, ot, n, _), !mro_lookup(p, ot, n, _), @@ -699,8 +722,7 @@ expr_type_untypable(p, e, "attribute_absent_on_type") :- // and no amount of field typing will reach it. Merging them would put the largest single // shape in the residual behind a name that suggests the wrong fix. expr_type_untypable(p, e, "class_attribute_absent") :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type_class_object(p, obj, ot), !type_attr_field(p, ot, n, _), !mro_lookup(p, ot, n, _), diff --git a/graph/python/engine/framework-behavior/dispatch.dl b/graph/python/engine/framework-behavior/dispatch.dl index b542838d..f7277835 100644 --- a/graph/python/engine/framework-behavior/dispatch.dl +++ b/graph/python/engine/framework-behavior/dispatch.dl @@ -292,9 +292,15 @@ py_fixture_request(req, name) :- py_fixture_param(req, name), // below. Only in a requester's own body: a helper function calling it is not collected, // and which test calls the helper is a call edge's question, not this rule's. .decl py_fixture_value_call(req:symbol, e:symbol) -py_fixture_value_call(req, e) :- py_fixture_requester(req), - call_decl("client", "METHOD_CALL", fn, _, e, site), py_fixture_value_method(fn), - call_context("client", _, req, _, _, site). +// SELECTIVE ATOM FIRST: there is one fixture-value method name and, on most projects, +// few or no calls to it — while there are tens of thousands of requesters. Written +// requester-first, the planner walked every requester's whole call context before the +// one-tuple filter said no: 295s to derive ZERO rows on a 526k-LOC subject. This order +// costs what the answer costs. +py_fixture_value_call(req, e) :- py_fixture_value_method(fn), + call_decl("client", "METHOD_CALL", fn, _, e, site), + call_context("client", _, req, _, _, site), + py_fixture_requester(req). // (a) the name written as a string literal; py_fixture_request(req, name) :- py_fixture_value_call(req, e), call_arg(e, "0", a), expr_node("client", "LITERAL", _, name, a), name != "". @@ -320,7 +326,7 @@ py_argnames_sep_at(h, s, i) :- py_parametrize_argnames(h, s), py_argnames_sep(se // A whole token: led by the string's start, a separator or a space, and followed by its // end, a separator or a space, so `server` is not found inside `server_url`. .decl py_argnames_found(h:symbol, name:symbol, i:number) -py_argnames_found(h, p, i) :- py_fixture_value_param(h, p), py_parametrize_argnames(h, s), +py_argnames_found(h, p, i) :- py_argnames_demand(h, p), py_parametrize_argnames(h, s), strlen(s) > strlen(p), i = range(0, strlen(s) - strlen(p) + 1), substr(s, i, strlen(p)) = p. .decl py_argnames_led(h:symbol, name:symbol, i:number) py_argnames_led(h, p, 0) :- py_argnames_found(h, p, 0). @@ -340,10 +346,15 @@ py_value_row_kind("LIST"). py_argnames_pos(h, p, k) :- py_argnames_name_at(h, p, i), py_parametrize_argnames(h, s), k = count : { py_argnames_sep_at(h, s, j), j < i }. // ... or, for argnames written as a list or tuple of strings, its element position there. -py_argnames_pos(h, p, k) :- py_fixture_value_param(h, p), +py_argnames_pos(h, p, k) :- py_argnames_demand(h, p), decorator_expr("client", de, h), call_arg(de, "0", a), expr_parent("client", a, "ELEMENT", ks, el), expr_node("client", "LITERAL", _, p, el), k = to_number(ks). +// the argument names whose position is asked for: one a getfixturevalue call reads, and +// every argument the decorator supplies directly (its values type the parameter, below) +.decl py_argnames_demand(h:symbol, p:symbol) +py_argnames_demand(h, p) :- py_fixture_value_param(h, p). +py_argnames_demand(h, p) :- py_parametrize_direct(req, p), py_parametrize_on(req, h). .decl py_fixture_value_param(h:symbol, p:symbol) py_fixture_value_param(h, p) :- py_fixture_value_call(req, e), call_arg(e, "0", a), expr_node("client", "NAME_REFERENCE", _, p, a), @@ -356,6 +367,70 @@ py_fixture_request(req, name) :- py_fixture_value_call(req, e), expr_parent("client", tup, "ELEMENT", to_string(k), el), expr_node("client", "LITERAL", _, name, el), name != "". +// ── PARAMETRIZE AND FIXTURE PARAMS: the VALUES the runner hands an argument ── +// @pytest.mark.parametrize("field_cls", [String, Integer]) -> field_cls is one of the classes +// @pytest.mark.parametrize("obj, n", [(Box(), 1), pytest.param(Bag(), 2)]) +// @pytest.fixture(params=(Converter, BaseConverter)) +// def converter_cls(request): return request.param -> one of the classes +// The runner calls the test once per row with the row's values in place of the argument, +// so each value is an argument reaching the parameter exactly as one written at a call +// site is (resolution/value-flow.dl types it). Only values WRITTEN in the decorator, or in +// a display a single-write name in this module (or one it imports the name from) holds; +// a value list built by a call is not read. +.decl py_pz_rows(h:symbol, rows:symbol) +py_pz_rows(h, a) :- py_parametrize_on(_, h), decorator_expr("client", de, h), call_arg(de, "1", a). +py_pz_rows(h, a) :- py_parametrize_on(_, h), decorator_expr("client", de, h), call_kwarg(de, k, a), py_parametrize_argvalues_kw(k). +py_pz_rows(h, a) :- py_fixture_deco_on(_, h), decorator_expr("client", de, h), call_kwarg(de, k, a), py_fixture_params_kw(k). +// the display a rows expression is: written in place, or held by a single-write name +.decl py_pz_display(rows:symbol, d:symbol) +py_pz_display(a, a) :- py_pz_rows(_, a), py_value_row_kind(k), expr_node("client", k, _, _, a). +py_pz_display(a, d) :- py_pz_rows(_, a), + expr_binding("client", b, ctx, a), ctx != "STORE", binding_lookup("client", b, b2), + py_pz_name_display(b2, d). +.decl py_pz_name_display(b:symbol, d:symbol) +py_pz_name_display(b, d) :- binding_rebinding("client", "1", _, _, b), + expr_binding("client", b, "STORE", tgt), assign_pair("client", tgt, d), + py_value_row_kind(k), expr_node("client", k, _, _, d). +// `from tests.base import ALL_FIELDS`: the module-level binding of that name in the module +py_pz_name_display(b, d) :- import_binding("client", b, i), + import_alias("client", orig, _, i), orig != "", import_relative("client", _, pkg, i), pkg != "", + module_decl("client", _, pkg, _, mod), + binding_decl("client", orig, _, "MODULE_LEVEL", _, mb), binding_owner("client", mod, _, mb), + binding_rebinding("client", "1", _, _, mb), + expr_binding("client", mb, "STORE", tgt), assign_pair("client", tgt, d), + py_value_row_kind(k), expr_node("client", k, _, _, d). +// a row, with `pytest.param(v, ..., id=...)` read as its positional values +.decl py_pz_row(h:symbol, row:symbol) +py_pz_row(h, r) :- py_pz_rows(h, a), py_pz_display(a, d), expr_parent("client", d, "ELEMENT", _, r). +.decl py_pz_param_call(row:symbol) +py_pz_param_call(r) :- py_pz_row(_, r), call_of_expr(r, site), call_name(site, w), py_param_row_wrapper(w). +// the value at one argument's position in a row: the whole row (one argument name), else +// the row's k-th element +.decl py_pz_single(h:symbol) +py_pz_single(h) :- py_parametrize_argnames(h, s), !py_argnames_sep_at(h, s, _). +py_pz_single(h) :- py_fixture_deco_on(_, h). +.decl py_pz_value(h:symbol, k:number, v:symbol) +py_pz_value(h, 0, r) :- py_pz_single(h), py_pz_row(h, r), !py_pz_param_call(r). +py_pz_value(h, 0, v) :- py_pz_single(h), py_pz_row(h, r), py_pz_param_call(r), call_arg(r, "0", v). +py_pz_value(h, k, v) :- !py_pz_single(h), py_pz_row(h, r), !py_pz_param_call(r), + py_value_row_kind(rk), expr_node("client", rk, _, _, r), + expr_parent("client", r, "ELEMENT", ks, v), k = to_number(ks). +py_pz_value(h, k, v) :- !py_pz_single(h), py_pz_row(h, r), py_pz_param_call(r), + call_arg(r, ks, v), k = to_number(ks). +// the parameter each value reaches: a test's directly parametrized argument ... +.decl py_pz_param_value(ph:symbol, v:symbol) +py_pz_param_value(ph, v) :- py_parametrize_direct(req, name), py_parametrize_on(req, h), + !py_pz_single(h), py_argnames_pos(h, name, k), py_pz_value(h, k, v), + param_decl("client", name, _, _, req, ph). +py_pz_param_value(ph, v) :- py_parametrize_direct(req, name), py_parametrize_on(req, h), + py_parametrize_argname(req, h, name), py_pz_single(h), py_pz_value(h, 0, v), + param_decl("client", name, _, _, req, ph). +// ... and, in a fixture declared with params=, what `request.param` evaluates to +.decl py_pz_request_param(e:symbol, v:symbol) +py_pz_request_param(e, v) :- py_fixture_deco_on(f, h), py_pz_value(h, 0, v), + py_fixture_request_param(rq), param_decl("client", rq, _, _, f, ph), + py_request_param_attr(pa), attr_access("client", e, pa, obj), expr_names_param("client", obj, ph). + // (0) a fixture a CLASS declares serves the tests of that class and of its subclasses, // and nothing else; for them it wins over the module's fixture of the name. Read as a // plain same-module fixture it was also handed to a sibling class's tests, and the @@ -406,6 +481,53 @@ py_fixture_requested(m) :- py_fixture_injection(_, m, _). framework_edge(from, to, "fixture_injection", name, "by_name") :- py_fixture_injection(from, to, name). +// ── 2a. THE SAME LOOKUP, FROM SYNTAX ALONE ────────────────────────────────────── +// resolution/value-flow.dl types a parameter by the fixture serving it, so it needs the +// runner's lookup INSIDE the resolution fixpoint. py_fixture_injection cannot be read +// there: a fixture a conftest star-imports is found through module_member_method, which +// is resolution, and the nearest-conftest MAX over it would then be a cyclic aggregate +// souffle refuses to stratify. This chain keeps the runner's order (class, own module, +// nearest conftest by its declaring file) and leaves out the two clauses that need +// resolution — a star-imported fixture and a pytest_plugins one. Those still reach their +// tests through the injection edge; they only go untyped. +.decl py_fixs_in_file(fix:symbol, file:symbol) +py_fixs_in_file(fix, p) :- py_fixture_decl(fix, _), method_file("client", p, fix). +.decl py_fixs_same_module(req:symbol, name:symbol, fix:symbol) +py_fixs_same_module(req, name, fix) :- py_fixture_request(req, name), + py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _), + !py_fixture_class_shadowed(req, name), + method_file("client", p, req), py_fixs_in_file(fix, p). +.decl py_fixs_conftest_cand(req:symbol, name:symbol, fix:symbol, depth:number) +py_fixs_conftest_cand(req, name, fix, dl) :- py_fixture_request(req, name), + py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _), + py_fixs_in_file(fix, cp), py_fixture_scope_file(c), + strlen(cp) >= strlen(c), substr(cp, strlen(cp) - strlen(c), strlen(c)) = c, + d = substr(cp, 0, strlen(cp) - strlen(c)), + method_file("client", p, req), + strlen(p) > strlen(d), substr(p, 0, strlen(d)) = d, dl = strlen(d). +.decl py_fixs_nearest(req:symbol, name:symbol, depth:number) +py_fixs_nearest(req, name, m) :- py_fixs_conftest_cand(req, name, _, _), + m = max dl : { py_fixs_conftest_cand(req, name, _, dl) }. +.decl py_fixs_injection(from:symbol, to:symbol, name:symbol) +py_fixs_injection(req, fix, name) :- py_fixture_in_class(req, name, fix). +py_fixs_injection(req, fix, name) :- py_fixs_same_module(req, name, fix). +py_fixs_injection(req, fix, name) :- py_fixs_conftest_cand(req, name, fix, dl), + !py_fixture_class_shadowed(req, name), !py_fixs_same_module(req, name, _), + py_fixs_nearest(req, name, dl). + +// ── 2b. FIXTURE VALUE (what the runner hands the parameter) ──────────────────── +// py_fixture_value_type(Fixture, Type) — the value a fixture hands over: its return or +// yield value's type, or its declared return. Consumed by resolution/value-flow.dl, where +// the runner's call `test(fixture_value)` is one more argument reaching a parameter. +.decl py_fixture_value_type(fix:symbol, t:symbol) +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + method_return_value_expr("client", fix, e), expr_type("client", e, t). +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + method_declared_return_type("client", fix, d), declared_dispatch("client", d, t). +py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _), + expr_node("client", "YIELD", _, _, y), expr_ultimate_method("client", y, fix), + expr_parent("client", y, "YIELD_VALUE", _, v), expr_type("client", v, t). + // ───────────────────────────────────────────────────────────────────────────── // 3. URL DISPATCH (route table -> view) // ───────────────────────────────────────────────────────────────────────────── diff --git a/graph/python/engine/framework-behavior/library-callbacks.dl b/graph/python/engine/framework-behavior/library-callbacks.dl new file mode 100644 index 00000000..eef361c8 --- /dev/null +++ b/graph/python/engine/framework-behavior/library-callbacks.dl @@ -0,0 +1,371 @@ +// ============================================================================ +// FRAMEWORK-BEHAVIOR · LIBRARY CALLBACKS (the hop a dependency makes back into the client) +// +// ── THE GAP ───────────────────────────────────────────────────────────────── +// The engine reads a dependency's signatures, never its bodies. So when a library calls +// BACK into client code, the call graph stops at the library frame: a test client's +// `client.get("/")` runs the `open()` a client subclass overrides and then the +// application object it was built around (`app(environ, start_response)`); a CLI test +// runner's `runner.invoke(cli)` runs `cli.main()`, which runs the command's callback; an +// `ExitStack.enter_context(cm)` runs `cm.__enter__`. Measured on a 16-repository Python +// oracle, a library frame is the first broken hop behind most test-impact misses on the +// web-framework subject: the test calls the library, the library calls the code under +// change, and the graph has only the first half. +// +// ── WHERE THE KNOWLEDGE COMES FROM ────────────────────────────────────────── +// graph/python/libsum/libsum.py reads the installed library's SOURCE once (cached by the +// installed distributions and the client's imports, never solved here) and stages these tables +// beside the client IR, for the library classes and functions the client can name: +// py_lib_callback(Callee, Root, Pos, Path, Member, Via, Type) +// calling the library callable Callee hands back `Member` on the object that arrived +// as its parameter Root (written by the client at position Pos, -1 = the receiver +// `self`), after following the attribute Path ("" = the object itself); Via is the +// library function where that invocation is written; Type the class the library declares that +// object to be, "" when it declares none +// py_lib_ancestor(Class, Ancestor) a library class's library ancestors +// py_lib_field_init(Class, Field, Param, Pos) +// constructing Class stores the argument Param on the new object as Field +// py_lib_alias(Alias, Canonical) +// a name a package re-exports +// py_lib_decorates(Decorator, Form, Class, Field) +// `@Decorator` (DIRECT) or `@Decorator(...)` (FACTORY) turns a def into a Class whose Field +// holds it; Class "" when the def itself comes back +// No row for a library written in C: a declared blind spot, never a guess. +// +// ── WHAT IT EMITS, AND WHY NOT AS A CALL ──────────────────────────────────── +// lib_callback_edge(Caller, Target, Callee, Member, Via, How) +// Caller the client method containing the call into the library +// Target the client method the library is summarised to call back +// Callee the library callable the client called (canonical name) +// How receiver Target is a member of the object the call was made on +// (a client subclass overriding what the library's own code calls) +// receiver_field ... of an object the library holds in a field of it +// argument ... of an object (or the callable itself) passed as an argument +// argument_field ... of an object held in a field of an argument +// The contract of this phase holds: it only ADDS a relation. The hop is a library's +// behaviour summarised from its source, joined to the client by a member NAME on a type +// the client handed over; a resolved call is stronger knowledge, so call_chain_edge is +// untouched and a consumer opts in (impact walks it as its own hop, `library callback`). +// No rule here names a library or a framework: every name comes from the staged tables. +// ============================================================================ + +// ── the dotted path a client expression spells through an unlinked import ─── +// `Client` after `from webkit.testing import Client`, `clikit.command` after `import clikit`: the import's path, +// then each attribute read off it. Only an import that linked to nothing in the client starts one. +.decl lcb_path_of(e:symbol, p:symbol) +lcb_path_of(e, p) :- + expr_binding("client", b, ctx, e), ctx != "STORE", + binding_lookup("client", b, b2), import_unlinked(b2, p). +lcb_path_of(e, cat(p, cat(".", n))) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), n != "", + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), lcb_path_of(obj, p), strlen(p) < 200. + +// ── its canonical name, following the package's re-exports ────────────────── +.decl lcb_named(p:symbol) +lcb_named(p) :- lcb_path_of(_, p). +.decl lcb_canon(p:symbol, c:symbol) +lcb_canon(p, c) :- lcb_named(p), py_lib_alias(p, c). +lcb_canon(p, p) :- lcb_named(p), !py_lib_alias(p, _). +.decl lcb_expr_names(e:symbol, c:symbol) +lcb_expr_names(e, c) :- lcb_path_of(e, p), lcb_canon(p, c). + +// ── a client class whose base is a library class ───────────────────────────── +// Written `class AppClient(Client)` with `Client` imported from a dependency that was not +// staged: the base resolves to nothing and type_base_unresolved records it by text. Here it +// is named by the import the text binds to, and inherited through client subclasses. +.decl lcb_direct_ext_base(t:symbol, c:symbol) +lcb_direct_ext_base(t, c) :- + type_base_slot("client", t, _, "NAME", _, _, bh), base_expr("client", bh, e), lcb_expr_names(e, c). +// a dotted base, `class Store(abc.Mapping)` after `from collections import abc`: the attribute chain names it +lcb_direct_ext_base(t, c) :- + type_base_slot("client", t, _, "DOTTED_NAME", _, _, bh), base_expr("client", bh, e), lcb_expr_names(e, c). +lcb_direct_ext_base(t, c) :- + type_base_slot("client", t, _, "SUBSCRIPT", _, _, bh), base_expr("client", bh, e), + expr_parent("client", e, "SUBSCRIPT_OBJECT", _, obj), lcb_expr_names(obj, c). +.decl lcb_ext_class(t:symbol, c:symbol) +lcb_ext_class(t, c) :- lcb_direct_ext_base(t, c). +lcb_ext_class(t, c) :- type_ancestor("client", t, a), lcb_direct_ext_base(a, c). + +// a client type and every client subclass of it: the object a library calls back may be a +// subclass of the type the call site was written against +.decl lcb_self_type(t:symbol, s:symbol) +lcb_self_type(t, t) :- lcb_ext_class(t, _). +lcb_self_type(t, s) :- lcb_ext_class(t, _), type_ancestor("client", s, t). + +.decl lcb_has_rows(c:symbol) +lcb_has_rows(c) :- py_lib_callback(c, _, _, _, _, _, _). + +// ── lcb_entry(Site, Callee, SelfType) — a client call into a summarised library callable ── +// SelfType is the client type the library's `self` is, or "" when the receiver is not a +// client object. +.decl lcb_entry(site:symbol, callee:symbol, selfT:symbol) +// (a) a member the client type inherits from its library base and does not define itself: +// `client.get("/")` on an AppClient-like subclass, `self.get(...)` inside one +lcb_entry(site, q, t) :- + call_receiver_object(site, obj), expr_type("client", obj, t), + call_name(site, n), n != "", + !expr_call_candidate(site, _), + lcb_ext_class(t, c), q = cat(c, cat(".", n)), lcb_has_rows(q). +// (b) super().n(...) whose MRO leaves the client for a library base +lcb_entry(site, q, a) :- + call_recv_kind(site, "SUPER"), call_super_anchor(site, _, a), + call_name(site, n), n != "", + !expr_call_candidate(site, _), + lcb_ext_class(a, c), q = cat(c, cat(".", n)), lcb_has_rows(q). +// (c) a library function or class called by a name the client imported +lcb_entry(site, c, "") :- + call_callee_is_value(site), call_callee_expr(site, callee), + lcb_expr_names(callee, c), lcb_has_rows(c). +// (d) a member of a library object the client built: `runner = CliRunner(); runner.invoke(cli)`, or one a +// fixture or a helper returned. Its `self` is a library object, so it has no client type; what the client +// gave its constructor is reached through the allocation (lcb_lobj_entry below). +lcb_entry(site, q, "") :- lcb_lobj_entry(site, q, _). +// (e) constructing a client class whose constructor is the library base's: its __init__ may +// call members the client overrides +lcb_entry(site, c, t) :- + call_constructs_type(site, t), lcb_ext_class(t, c), + !type_constructor("client", t, _), lcb_has_rows(c). + +// (f) a hand-back to a member the client object does not define but INHERITS from a library class: the +// library calls `cli.main()` on the command it was given, `main` is the library base's own, and that +// method's hand-backs apply to the same object (its make_context override). Same site, same caller. +lcb_entry(site, q2, t) :- + lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _), + expr_type("client", a, t), !mro_lookup("client", t, n, _), + lcb_ext_class(t, c), q2 = cat(c, cat(".", n)), lcb_has_rows(q2). +lcb_entry(site, q2, t) :- + lcb_entry(site, q, t), t != "", py_lib_callback(q, "self", _, "", n, _, _), + !mro_lookup("client", t, n, _), + lcb_ext_class(t, c), q2 = cat(c, cat(".", n)), lcb_has_rows(q2). + +// ── lcb_ext_instance(Expr, Class, Alloc) — an expression holding an object of a library class ── +// Alloc is the call site that constructed it. Followed through a single-binding local, a client function's +// return value, and the fixture a test parameter is filled from. +.decl lcb_ext_instance(e:symbol, c:symbol, alloc:symbol) +lcb_ext_instance(e, c, site) :- + call_of_expr(e, site), call_callee_is_value(site), call_callee_expr(site, callee), + lcb_expr_names(callee, c), lcb_is_class(c). +lcb_ext_instance(e, c, al) :- + expr_binding("client", bref, ctx, e), ctx != "STORE", + binding_lookup("client", bref, bind), + binding_rebinding("client", "1", _, _, bind), + expr_binding("client", bind, "STORE", tgt), + assign_pair("client", tgt, val), + lcb_ext_instance(val, c, al). +lcb_ext_instance(e, c, al) :- + call_of_expr(e, site), expr_call_candidate(site, m), + method_return_value_expr("client", m, r), lcb_ext_instance(r, c, al). +lcb_ext_instance(e, c, al) :- + expr_names_param("client", e, ph), param_decl("client", pn, _, _, req, ph), + py_fixs_injection(req, fix, pn), method_return_value_expr("client", fix, r), lcb_ext_instance(r, c, al). +lcb_ext_instance(e, c, al) :- + expr_names_param("client", e, ph), param_decl("client", pn, _, _, req, ph), + py_fixs_injection(req, fix, pn), + expr_node("client", "YIELD", _, _, y), expr_ultimate_method("client", y, fix), + expr_parent("client", y, "YIELD_VALUE", _, v), lcb_ext_instance(v, c, al). +.decl lcb_is_class(c:symbol) +lcb_is_class(c) :- py_lib_field_init(c, _, _, _). +lcb_is_class(c) :- py_lib_callback(c, "self", _, _, _, _, _). + +// ── LIBRARY OBJECTS THE CLIENT HOLDS ──────────────────────────────────────── +// lcb_lobj(Expr, Class, Obj): Expr holds a library object of Class, identified by Obj -- the call site that +// constructed it, or the client function a library decorator wrapped into it (`@command() def greet()` makes +// `greet` a Command whose `callback` is the def). Its fields are what the client gave it. +.decl lcb_lobj(e:symbol, c:symbol, obj:symbol) +lcb_lobj(e, c, al) :- lcb_ext_instance(e, c, al). +lcb_lobj(e, c, m) :- lcb_names_def(e, m), lcb_deco_obj(m, c, _). + +// the def a name denotes, decorated or not (the decorated name's VALUE is what the decorator returned) +.decl lcb_names_def(e:symbol, m:symbol) +lcb_names_def(e, m) :- expr_binding("client", bref, ctx, e), ctx != "STORE", binding_lookup("client", bref, b), + method_declaring_binding("client", b, m). +lcb_names_def(e, m) :- expr_binding("client", bref, ctx, e), ctx != "STORE", binding_lookup("client", bref, b), + import_binds_method("client", b, m). +lcb_names_def(e, m) :- expr_attr_names_method("client", e, m). + +// the library callable a decorator expression applies: `@q` (direct) or `@q(...)` (factory) +.decl lcb_deco_callable(d:symbol, q:symbol, form:symbol) +lcb_deco_callable(d, q, "DIRECT") :- decorator_expr("client", e, d), !call_of_expr(e, _), lcb_expr_names(e, q). +lcb_deco_callable(d, q, "FACTORY") :- decorator_expr("client", e, d), call_of_expr(e, site), + call_callee_expr(site, callee), lcb_expr_names(callee, q). +lcb_deco_callable(d, q, "FACTORY") :- decorator_expr("client", e, d), call_of_expr(e, site), + call_receiver_object(site, obj), expr_type("client", obj, t), call_name(site, n), !mro_lookup("client", t, n, _), + lcb_ext_class(t, c), q = cat(c, cat(".", n)). +// a decorator that hands the function straight back (registers it, records an option on it) +.decl lcb_deco_identity(d:symbol) +lcb_deco_identity(d) :- lcb_deco_callable(d, q, form), py_lib_decorates(q, form, "", ""). +lcb_deco_identity(d) :- decorator_builtin("client", bk, d), decorator_transparent(bk). +// the def m enters application stage k unchanged: every decorator applied before it handed it straight back +.decl lcb_deco_blocked(m:symbol, k:number) +lcb_deco_blocked(m, k) :- deco_at(m, k, _), deco_at(m, k2, d2), k2 < k, !lcb_deco_identity(d2), !deco_stage(m, k2 + 1, m). +.decl lcb_deco_obj(m:symbol, c:symbol, f:symbol) +lcb_deco_obj(m, c, f) :- deco_at(m, k, d), !lcb_deco_blocked(m, k), lcb_deco_callable(d, q, form), + py_lib_decorates(q, form, c, f), c != "". + +// ── entries on a library object, and what its fields hand back ────────────── +.decl lcb_lobj_entry(site:symbol, callee:symbol, obj:symbol) +lcb_lobj_entry(site, q, o) :- + call_receiver_object(site, recv), lcb_lobj(recv, c, o), + call_name(site, n), n != "", + !expr_call_candidate(site, _), + q = cat(c, cat(".", n)), lcb_has_rows(q). +// a library object handed to a library callable that calls one of the object's own (library) members +lcb_lobj_entry(site, q2, o) :- + lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _), lcb_lobj(a, c, o), + q2 = cat(c, cat(".", n)), lcb_has_rows(q2). + +// what a library object's field holds that offers member n: the argument its construction was given, or the +// function a decorator wrapped (called as `__call__`) +.decl lcb_lobj_field_target(o:symbol, f:symbol, n:symbol, m:symbol) +lcb_lobj_field_target(al, f, n, m) :- lcb_alloc_field(al, f, a), lcb_member_of_expr(a, n, m). +lcb_lobj_field_target(m, f, "__call__", m) :- lcb_deco_obj(m, _, f), lcb_field_wanted(m, f). +.decl lcb_field_wanted(o:symbol, f:symbol) +lcb_field_wanted(o, f) :- lcb_lobj_entry(_, q, o), py_lib_callback(q, "self", _, f, _, _, _), f != "". +lcb_field_wanted(o, f) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, f, _, _, _), f != "", lcb_lobj(a, _, o), + lcb_entry(site, q, _). + +// what the constructor of a library object stored in a field, as the client wrote it at the allocation +.decl lcb_alloc_field(al:symbol, f:symbol, a:symbol) +lcb_alloc_field(al, f, a) :- + lcb_field_wanted(al, f), call_callee_expr(al, callee), lcb_expr_names(callee, c), + py_lib_field_init(c, f, _, pos), call_arg(al, ps, a), to_number(ps) = to_number(pos). +lcb_alloc_field(al, f, a) :- + lcb_field_wanted(al, f), call_callee_expr(al, callee), lcb_expr_names(callee, c), + py_lib_field_init(c, f, pn, _), call_kwarg(al, pn, a). + +// ── the argument the client wrote for a summarised parameter ───────────────── +.decl lcb_arg(site:symbol, callee:symbol, root:symbol, a:symbol) +lcb_arg(site, q, r, a) :- + lcb_entry(site, q, _), py_lib_callback(q, r, pos, _, _, _, _), r != "self", + to_number(pos) >= 0, call_arg(site, ps, a), to_number(ps) = to_number(pos). +lcb_arg(site, q, r, a) :- + lcb_entry(site, q, _), py_lib_callback(q, r, _, _, _, _, _), r != "self", + call_kwarg(site, r, a). + +// ── what a library-held field of a client object holds ─────────────────────── +// lcb_field_holds(T, Field, Expr): an object of client type T was built with Expr as the +// argument the library constructor stores in Field (the construction of T or of a client +// subclass, or T's own super().__init__(...) call) +.decl lcb_ctor_store(t:symbol, f:symbol, param:symbol, pos:symbol) +lcb_ctor_store(t, f, pn, pos) :- lcb_ext_class(t, c), py_lib_field_init(c, f, pn, pos). +.decl lcb_field_holds(t:symbol, f:symbol, e:symbol) +lcb_field_holds(t, f, a) :- + lcb_ctor_store(t, f, _, pos), lcb_self_type(t, s), call_constructs_type(site, s), + call_arg(site, ps, a), to_number(ps) = to_number(pos). +lcb_field_holds(t, f, a) :- + lcb_ctor_store(t, f, pn, _), lcb_self_type(t, s), call_constructs_type(site, s), + call_kwarg(site, pn, a). +lcb_field_holds(t, f, a) :- + lcb_ctor_store(t, f, _, pos), call_recv_kind(site, "SUPER"), call_super_anchor(site, _, t), + call_name(site, "__init__"), call_arg(site, ps, a), to_number(ps) = to_number(pos). +lcb_field_holds(t, f, a) :- + lcb_ctor_store(t, f, pn, _), call_recv_kind(site, "SUPER"), call_super_anchor(site, _, t), + call_name(site, "__init__"), call_kwarg(site, pn, a). + +// ── the client member an object expression offers under a name ─────────────── +// the type the expression has, or a client subclass of it; `__call__` on an expression that +// IS a function or a bound method is that function +.decl lcb_member_of_expr(e:symbol, n:symbol, m:symbol) +lcb_member_of_expr(e, n, m) :- + lcb_wanted(e, n), expr_type("client", e, t), lcb_sub(t, s), mro_lookup("client", s, n, m), + method_owner("client", _, m). +lcb_member_of_expr(e, "__call__", m) :- lcb_wanted(e, "__call__"), expr_denotes_method("client", e, m). +lcb_member_of_expr(e, "__call__", m) :- lcb_wanted(e, "__call__"), expr_names_method("client", e, m). +.decl lcb_sub(t:symbol, s:symbol) +lcb_sub(t, t) :- lcb_wanted(e, _), expr_type("client", e, t). +lcb_sub(t, s) :- lcb_wanted(e, _), expr_type("client", e, t), type_ancestor("client", s, t). + +// the member of a client object's own field, as the client wrote it (`self.app = app`) +.decl lcb_member_of_field(t:symbol, f:symbol, n:symbol, m:symbol) +lcb_member_of_field(t, f, n, m) :- + lcb_wanted_field(t, f, n), field_decl("client", f, owner, _, fh), + (owner = t ; type_ancestor("client", t, owner)), + field_type("client", fh, x), mro_lookup("client", x, n, m), method_owner("client", _, m). + +// demand: only the (expression, member) pairs a summary asks about are looked up +.decl lcb_wanted(e:symbol, n:symbol) +lcb_wanted(a, n) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _), lcb_entry(site, q, _). +lcb_wanted(e, n) :- lcb_entry(_, q, t), t != "", py_lib_callback(q, "self", _, f, n, _, _), f != "", + lcb_field_holds(t, f, e). +lcb_wanted(a, n) :- lcb_alloc_field(al, f, a), lcb_lobj_entry(_, q, al), py_lib_callback(q, "self", _, f, n, _, _). +lcb_wanted(e, n) :- lcb_alloc_field(al, f, e), lcb_lobj(a, _, al), lcb_arg(_, q, r, a), py_lib_callback(q, r, _, f, n, _, _). +lcb_wanted(e, n) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, f, n, _, _), f != "", lcb_entry(site, q, _), + expr_type("client", a, t), lcb_field_holds(t, f, e). +.decl lcb_wanted_field(t:symbol, f:symbol, n:symbol) +lcb_wanted_field(s, f, n) :- lcb_entry(_, q, t), t != "", py_lib_callback(q, "self", _, f, n, _, _), f != "", + !contains(".", f), lcb_self_type(t, s). +lcb_wanted_field(t, f, n) :- lcb_arg(site, q, r, a), lcb_entry(site, q, _), py_lib_callback(q, r, _, f, n, _, _), f != "", + !contains(".", f), expr_type("client", a, t). + +// ── the edges ─────────────────────────────────────────────────────────────── +// lcb_hit(Site, Caller, Target, Callee, Root, Path, Member, Via, How): one summary row joined to the client object +// the call actually hands over. +.decl lcb_hit(site:symbol, caller:symbol, m:symbol, q:symbol, r:symbol, f:symbol, n:symbol, via:symbol, how:symbol) +// the receiver itself: a member the client type (or a subclass) defines over the library's +lcb_hit(site, caller, m, q, "self", "", n, via, "receiver") :- + lcb_entry(site, q, t), t != "", site_caller(site, caller), + py_lib_callback(q, "self", _, "", n, via, _), + lcb_self_type(t, s), mro_lookup("client", s, n, m), method_owner("client", _, m). +// a field of the receiver the library constructor filled from a client argument +lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :- + lcb_entry(site, q, t), t != "", site_caller(site, caller), + py_lib_callback(q, "self", _, f, n, via, _), f != "", + lcb_field_holds(t, f, e), lcb_member_of_expr(e, n, m). +// a field of a library object the client holds (built by it, or made by a library decorator around its def) +lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :- + lcb_lobj_entry(site, q, o), site_caller(site, caller), + py_lib_callback(q, "self", _, f, n, via, _), f != "", + lcb_lobj_field_target(o, f, n, m). +lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :- + lcb_arg(site, q, r, a), site_caller(site, caller), lcb_entry(site, q, _), + py_lib_callback(q, r, _, f, n, via, _), f != "", + lcb_lobj(a, _, o), lcb_lobj_field_target(o, f, n, m). +// a field of the receiver the client itself assigned +lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :- + lcb_entry(site, q, t), t != "", site_caller(site, caller), + py_lib_callback(q, "self", _, f, n, via, _), f != "", + lcb_self_type(t, s), lcb_member_of_field(s, f, n, m). +// an argument, or the callable passed as one +lcb_hit(site, caller, m, q, r, "", n, via, "argument") :- + lcb_arg(site, q, r, a), site_caller(site, caller), + py_lib_callback(q, r, _, "", n, via, _), + lcb_member_of_expr(a, n, m). +// a field of an argument +lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :- + lcb_arg(site, q, r, a), site_caller(site, caller), + py_lib_callback(q, r, _, f, n, via, _), f != "", + expr_type("client", a, t), lcb_field_holds(t, f, e), lcb_member_of_expr(e, n, m). +lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :- + lcb_arg(site, q, r, a), site_caller(site, caller), + py_lib_callback(q, r, _, f, n, via, _), f != "", + expr_type("client", a, t), lcb_member_of_field(t, f, n, m). + +// ── WHEN THE OBJECT CANNOT BE TOLD: THE TYPE THE LIBRARY DECLARES ──────────── +// A row whose object the client side could not name (a loader passed through **options, a command a helper +// returns typed only as the library's base) is still a hand-back to SOME object of the type the library declares +// there. The client classes that can be that object are its client subclasses; the one whose member runs is the one +// that defines it. A class-hierarchy answer, used only for a row no client object answered at that site. +.decl lcb_row_hit(site:symbol, q:symbol, r:symbol, f:symbol, n:symbol) +lcb_row_hit(site, q, r, f, n) :- lcb_hit(site, _, _, q, r, f, n, _, _). +.decl lcb_cha_wanted(ty:symbol, n:symbol) +lcb_cha_wanted(ty, n) :- lcb_entry(_, q, _), py_lib_callback(q, r, _, f, n, _, ty), ty != "", (r != "self" ; f != ""). +.decl lcb_cha(ty:symbol, n:symbol, m:symbol) +lcb_cha(ty, n, m) :- lcb_cha_wanted(ty, n), lcb_ext_class(s, ty), mro_lookup("client", s, n, m), method_owner("client", _, m). +lcb_cha(ty, n, m) :- lcb_cha_wanted(ty, n), lcb_ext_class(s, c), py_lib_ancestor(c, ty), + mro_lookup("client", s, n, m), method_owner("client", _, m). + +// lib_callback_edge(Caller, Target, Callee, Member, Via, How): the client method Caller calls the library +// callable Callee, which is summarised (from the library's source) to invoke Member -- written in the library +// function Via -- on an object that resolves to the client method Target. How: receiver | receiver_field | +// argument | argument_field | declared_type. A library's behaviour joined by a member name, not a resolved call. +lib_callback_edge(caller, m, q, n, via, how) :- lcb_hit(_, caller, m, q, _, _, n, via, how). +lib_callback_edge(caller, m, q, n, via, "declared_type") :- + lcb_entry(site, q, _), site_caller(site, caller), + py_lib_callback(q, r, _, f, n, via, ty), ty != "", (r != "self" ; f != ""), + !lcb_row_hit(site, q, r, f, n), lcb_cha(ty, n, m). + +// lib_callback_site(Site, Callee): a client call site the engine left unresolved that enters the library callable +// Callee, which the library's own source declares (a member the client type inherits from its library base, a +// super() call past the client classes, a member of a library object the client built). Not a call to any +// same-named client method, so a consumer must not guess one by name. +lib_callback_site(site, q) :- lcb_entry(site, q, _), q != "". diff --git a/graph/python/engine/resolution/annotations.dl b/graph/python/engine/resolution/annotations.dl index 0d671aa9..2b9087a4 100644 --- a/graph/python/engine/resolution/annotations.dl +++ b/graph/python/engine/resolution/annotations.dl @@ -199,6 +199,25 @@ type_ref_element(p, parentRef, t) :- annotation_owner_module(p, owner, ok, mod), type_name_in_module(p, mod, tn, t). +// ── union_operand(Prov, UnionRefHash, OperandRefHash) — every operand of a union ── +// `|` is LEFT-associative, so `A | B | None` is `(A | B) | None`: A and B are not +// children of the annotation but grandchildren, under a nested UNION_PEP604, and the +// depth-1 clauses above saw only that nested union (which names no type) and None. Every +// rule reading "Optional[X] IS X" therefore lost the members of any union with three or +// more operands, on a parameter, a return, a field or a local alike. +union_operand(p, r, c) :- + type_ref(p, k, _, _, _, r), annotation_optional_kind(k), type_ref_nesting(p, r, _, _, c). +union_operand(p, r, c) :- + union_operand(p, r, u), type_ref(p, "UNION_PEP604", _, _, _, u), type_ref_nesting(p, u, _, _, c). +// the operands BELOW depth 1 (depth 1 is the clauses above), resolved, and by name +type_ref_element(p, r, t) :- + union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1", type_ref_resolved(p, t, c). +type_ref_element(p, r, t) :- + union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1", + type_ref(p, _, "GENERIC_ARGUMENT", tn, _, c), tn != "", + type_ref_owner(p, owner, ok, r), annotation_owner_module(p, owner, ok, mod), + type_name_in_module(p, mod, tn, t). + // ── annotation_owner_module(Prov, OwnerHash, OwnerKind, ModuleHash) ────────── // The module an annotation was written in, whatever kind of declaration owns it. annotation_owner_module(p, ph, "METHOD_PARAM", mod) :- diff --git a/graph/python/engine/resolution/attribute-lookup.dl b/graph/python/engine/resolution/attribute-lookup.dl index 4fbc0982..7b44fe2d 100644 --- a/graph/python/engine/resolution/attribute-lookup.dl +++ b/graph/python/engine/resolution/attribute-lookup.dl @@ -152,10 +152,15 @@ mro_lookup_data_shadow(p, t, n, f) :- // factory), while mro_lookup is upstream of it and uses negation — folding this in makes // that negation cyclic and souffle refuses the program. Consumed by // expression-resolution/callee-resolution.dl, which is downstream of both. +// The class-member lookup is pure syntax and was re-run inside the fixpoint every +// iteration (12s for 783 rows on a mid-size subject); materialized once, keyed (c, n), +// the rule starts from its recursive delta and probes it. +replaced_method_of(p, c, n, def) :- + method_owner(p, c, def), method_decl(p, n, _, _, _, def), + method_target_replaced(p, def). mro_lookup_decorated(p, t, n, r) :- mro_winner(p, t, n, c), - method_owner(p, c, def), method_decl(p, n, _, _, _, def), - method_target_replaced(p, def), + replaced_method_of(p, c, n, def), decorated_name_target(def, r). // ── mro_lookup_opaque(Prov, ReceiverType, Name) ────────────────────────────── @@ -312,6 +317,15 @@ type_call_target(p, t, m) :- mro_lookup(p, t, "__call__", m). type_property_getter(p, t, n, m) :- mro_lookup(p, t, n, m), method_kind(p, "PROPERTY_GETTER", _, m). +// ── type_property_accessor(Prov, TypeHash, Name, Kind, MethodHash) ─────────── +// `@x.setter` and `@x.deleter` are the other two halves: `obj.x = v` runs the setter and +// `del obj.x` the deleter, and like the getter neither has a call site. They share the +// getter's name and binding, so they are found on the class that WINS the name in the +// receiver's MRO, by their method kind, rather than through mro_lookup's one answer. +type_property_accessor(p, t, n, k, m) :- + mro_winner(p, t, n, c), method_owner(p, c, m), method_decl(p, n, _, _, _, m), + method_kind(p, k, _, m), (k = "PROPERTY_SETTER" ; k = "PROPERTY_DELETER"). + // ── A USER-WRITTEN DATA DESCRIPTOR IS THE SAME PROTOCOL (issue #326) ───────── // `@property` IS a data descriptor; the decorator is sugar over `__get__`/`__set__`. So // the clause above reads one spelling of the protocol and a class-level diff --git a/graph/python/engine/resolution/builtin-types.dl b/graph/python/engine/resolution/builtin-types.dl index c18052d5..457f3453 100644 --- a/graph/python/engine/resolution/builtin-types.dl +++ b/graph/python/engine/resolution/builtin-types.dl @@ -199,14 +199,12 @@ expr_builtin_type("client", aw, b) :- // keyed on expr_attr_of_self is self-only by construction and needs the general form // beside it. expr_builtin_type(p, e, b) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), annotation_builtin(p, f, "FIELD", b). expr_builtin_type(p, e, b) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_write_value(p, f, val), expr_builtin_type(p, val, b). @@ -474,8 +472,7 @@ expr_container_annotation_ref(p, src, r) :- // the same hop through ANY object whose type is known, not only self -- the fix that // #61 / #63 had to make three times over in the other typing files. expr_container_annotation_ref(p, src, r) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), type_ref_owner(p, f, "FIELD", r), type_ref(p, _, "FIELD_TYPE", _, _, r). diff --git a/graph/python/engine/resolution/builtins.dl b/graph/python/engine/resolution/builtins.dl index ca1e14d1..e08902e1 100644 --- a/graph/python/engine/resolution/builtins.dl +++ b/graph/python/engine/resolution/builtins.dl @@ -182,6 +182,18 @@ py_copy_module("copy"). py_copy_function("copy"). py_copy_function("deepcopy"). +// ── py_returns_arg(DottedName, Position) — a library call that hands back an argument ── +// Each returns, unchanged, the argument at Position (documented behaviour, not inference): +// functools.update_wrapper(wrapper, wrapped) -> wrapper (copies __name__/__doc__ onto it) +// typing.cast(T, value) -> value (a no-op at run time) +// A decorator written `return update_wrapper(wrapper, f)`, or the typed spelling +// `return t.cast(F, update_wrapper(wrapper, f))`, is therefore the same shape as one +// returning `wrapper` under @functools.wraps. Without these every method it decorates was +// decorator_replaced_target, and no call through the attribute reached anything +// (call-edge-generation/call_chain.dl). Neither module is staged in a client-only run. +py_returns_arg("functools.update_wrapper", "0"). +py_returns_arg("typing.cast", "1"). + // ── py_builtin_dynamic(Name) — the escape hatches ──────────────────────────── // These do not merely lack Python source; they make the PROGRAM unanalysable at that // point. A call to getattr/eval/exec means the engine cannot know what runs, and the @@ -358,3 +370,79 @@ class_definition_keyword("metaclass"). // (expression-resolution/expr-type.dl, clause a'), where the convention is the only // signal there is: a lambda has no annotation and no `def` to classify. instance_receiver_name("self"). + +// ── builtin_protocol_slot(Builtin, Dunder) / builtin_protocol_fallback ─────── +// The one-dunder builtins: `repr(x)` runs type(x).__repr__, and so on. str() falls back to +// __repr__ and bool() to __len__ when the class has no slot of its own. Read by +// call-edge-generation/call_chain.dl's BUILTIN PROTOCOL edges. +builtin_protocol_slot("repr", "__repr__"). +builtin_protocol_slot("str", "__str__"). +builtin_protocol_slot("len", "__len__"). +builtin_protocol_slot("hash", "__hash__"). +builtin_protocol_slot("bool", "__bool__"). +builtin_protocol_slot("iter", "__iter__"). +builtin_protocol_slot("next", "__next__"). +builtin_protocol_slot("abs", "__abs__"). +builtin_protocol_slot("format", "__format__"). +builtin_protocol_slot("reversed", "__reversed__"). +builtin_protocol_fallback("str", "__str__", "__repr__"). +builtin_protocol_fallback("bool", "__bool__", "__len__"). + +// ── operator_protocol_slot(Operator, Dunder, ReflectedDunder) ──────────────── +// `a + b` runs type(a).__add__, and type(b).__radd__ when that is missing or returns +// NotImplemented; a comparison runs the left operand's slot and the right operand's mirror +// (`a < b` -> b.__gt__, `a == b` -> b.__eq__). The language reference's data model, 3.3.8 +// (numeric) and 3.3.1 (rich comparison). Read by call-edge-generation/call_chain.dl's +// OPERATOR PROTOCOL edges. +operator_protocol_slot("+", "__add__", "__radd__"). +operator_protocol_slot("-", "__sub__", "__rsub__"). +operator_protocol_slot("*", "__mul__", "__rmul__"). +operator_protocol_slot("@", "__matmul__", "__rmatmul__"). +operator_protocol_slot("/", "__truediv__", "__rtruediv__"). +operator_protocol_slot("//", "__floordiv__", "__rfloordiv__"). +operator_protocol_slot("%", "__mod__", "__rmod__"). +operator_protocol_slot("**", "__pow__", "__rpow__"). +operator_protocol_slot("<<", "__lshift__", "__rlshift__"). +operator_protocol_slot(">>", "__rshift__", "__rrshift__"). +operator_protocol_slot("&", "__and__", "__rand__"). +operator_protocol_slot("|", "__or__", "__ror__"). +operator_protocol_slot("^", "__xor__", "__rxor__"). +operator_protocol_slot("==", "__eq__", "__eq__"). +operator_protocol_slot("!=", "__ne__", "__ne__"). +operator_protocol_slot("<", "__lt__", "__gt__"). +operator_protocol_slot("<=", "__le__", "__ge__"). +operator_protocol_slot(">", "__gt__", "__lt__"). +operator_protocol_slot(">=", "__ge__", "__le__"). +// `a != b` on a class with no __ne__ of its own runs object.__ne__, which calls __eq__. +operator_protocol_fallback("__ne__", "__eq__"). +// ── membership_operator(Operator) — `x in c` runs type(c).__contains__ ─────── +// and, for a class with no __contains__, iterates it (__iter__). Data model 3.3.7. +membership_operator("in"). +membership_operator("not in"). +membership_protocol_slot("__contains__"). +membership_protocol_fallback("__contains__", "__iter__"). +// ── unary_protocol_slot(Operator, Dunder) ──────────────────────────────────── +unary_protocol_slot("-", "__neg__"). +unary_protocol_slot("+", "__pos__"). +unary_protocol_slot("~", "__invert__"). +// ── truth testing — `if x:`, `while x:`, `not x`, `x and y`, `assert x` ─────── +// runs type(x).__bool__, or __len__ when the class has no __bool__ (data model 3.3.1, +// object.__bool__). truth_negation_operator is the one unary operator that truth-tests its +// operand; truth_test_root the statement contexts whose expression is truth-tested. +truth_negation_operator("not"). +truth_protocol_fallback("__bool__", "__len__"). +truth_test_root("IF_CONDITION"). +truth_test_root("WHILE_CONDITION"). +truth_test_root("ASSERT_CONDITION"). +truth_test_role("CONDITION"). +truth_test_role("COMPREHENSION_CONDITION"). + +// ── dict_view_name / dict_view_slot / dict_items_view — a mapping's views ──── +// `keys()` yields the key (generic position 0), `values()` the value (position 1), and +// `items()` a (key, value) pair. Read by resolution/iteration.dl. +dict_view_name("keys"). +dict_view_name("values"). +dict_view_name("items"). +dict_view_slot("keys", "0"). +dict_view_slot("values", "1"). +dict_items_view("items"). diff --git a/graph/python/engine/resolution/dispatch.dl b/graph/python/engine/resolution/dispatch.dl index f1f50d21..ed56e8cc 100644 --- a/graph/python/engine/resolution/dispatch.dl +++ b/graph/python/engine/resolution/dispatch.dl @@ -142,7 +142,31 @@ call_candidate_count(site, n) :- // ── call_dispatch_over_cap(SiteHash) ───────────────────────────────────────── call_dispatch_over_cap(site) :- - call_candidate_count(site, n), dispatch_capped_at(cap), n > cap. + call_candidate_count(site, n), dispatch_capped_at(cap), n > cap, + !call_computed_name_family(site). + +// ── call_computed_name_family(SiteHash) — a fan the PROGRAM wrote down ─────── +// The cap guards against a receiver type inferred too wide, where most of the fan is the +// engine's imprecision. A callee looked up by a name with a written prefix -- +// `getattr(self, f"visit_{...}")`, directly, through the local it was stored in, or +// through a method that returns it -- is the program's own dispatch table: every member +// named with the prefix is a target by construction, and a visitor with sixty visit_* +// methods is sixty targets, not sixty guesses. Refusing it dropped the visitor's whole +// body from every test that drives it. +method_returns_computed_name(m) :- + method_return_value_expr("client", m, e), getattr_computed_name(e, _, _). +computed_name_value(e) :- getattr_computed_name(e, _, _). +computed_name_value(e) :- + call_of_expr(e, s), expr_call_candidate(s, m), method_returns_computed_name(m). +call_computed_name_family(site) :- + call_callee_is_value(site), call_callee_expr(site, callee), computed_name_value(callee). +call_computed_name_family(site) :- + call_callee_is_value(site), call_callee_expr(site, callee), + expr_binding("client", b, ctx, callee), ctx != "STORE", + binding_lookup("client", b, b2), + expr_binding("client", b2, "STORE", tgt), + assign_pair("client", tgt, val), + computed_name_value(val). // ── expr_resolves_to_method(SiteHash, MethodHash) ──────────────────────────── // THE DISAMBIGUATED CALLEE SET the edge layer consumes. Same relation name as Java's, diff --git a/graph/python/engine/resolution/iteration.dl b/graph/python/engine/resolution/iteration.dl index 4f32f14b..76d50692 100644 --- a/graph/python/engine/resolution/iteration.dl +++ b/graph/python/engine/resolution/iteration.dl @@ -36,12 +36,19 @@ iter_pair(p, tgt, src) :- for_statement_roots("FOR_TARGET", "FOR_ITERABLE"). for_statement_roots("ASYNC_FOR_TARGET", "ASYNC_FOR_ITERABLE"). +// ── root_expr_at(Prov, RootContext, Scope, Line, Expr) — the pairing key, ONCE ── +// Both statement pairings below match a target root against a source root by scope and +// line; written inline, each pairing enumerated every root of one context and probed +// scope/span per candidate (22s between them for ~6K rows). Materialized with the key +// in the columns, each pairing is one indexed join. +root_expr_at(p, rc, s, line, e) :- + expr_root_context(p, rc, e), !expr_has_parent(p, e), + expr_scope(p, s, _, e), expr_span(p, line, _, e). + iter_pair(p, tgt, src) :- for_statement_roots(trc, irc), - expr_root_context(p, trc, tgt), !expr_has_parent(p, tgt), - expr_root_context(p, irc, src), !expr_has_parent(p, src), - expr_scope(p, s, _, tgt), expr_scope(p, s, _, src), - expr_span(p, line, _, tgt), expr_span(p, line, _, src). + root_expr_at(p, trc, s, line, tgt), + root_expr_at(p, irc, s, line, src). // ── with_pair(Prov, TargetExprHash, ContextExprHash) — `with X() as y` (#128) ── // The SAME shape as a for statement, and it lives here for that reason: the parser emits @@ -52,10 +59,8 @@ with_statement_roots("ASYNC_WITH_TARGET", "ASYNC_WITH_CONTEXT"). with_pair(p, tgt, cm) :- with_statement_roots(trc, crc), - expr_root_context(p, trc, tgt), !expr_has_parent(p, tgt), - expr_root_context(p, crc, cm), !expr_has_parent(p, cm), - expr_scope(p, s, _, tgt), expr_scope(p, s, _, cm), - expr_span(p, line, _, tgt), expr_span(p, line, _, cm). + root_expr_at(p, trc, s, line, tgt), + root_expr_at(p, crc, s, line, cm). // The two names this needs, stated as facts rather than written into a rule body: one is a // dunder the language defines, the other the typing spelling for "the enclosing class". @@ -93,6 +98,18 @@ with_target_declared(p, tgt, ct) :- method_decl(p, _, _, _, ret, m), self_return_name(ret). +// The same, WRITTEN rather than declared: an __enter__ with no return annotation whose +// return value is `self` (`def __enter__(self): return self`). The protocol hands the +// target what __enter__ returned, and that is the context manager itself. +with_target_declared(p, tgt, ct) :- + with_pair(p, tgt, cm), + expr_root_context(p, crc, cm), context_manager_enter_of(crc, en), + expr_type(p, cm, ct), + mro_lookup(p, ct, en, m), + !method_declared_return_type(p, m, _), + method_return_value_expr(p, m, e), + expr_node(p, "SELF_REFERENCE", _, _, e). + // AN __enter__ RETURN IS A DECLARED TYPE, not an exact one, so it goes out through // declared_dispatch and is registered with call_recv_declared in dispatch.dl. A declared // type escaping that gate has produced a confident-wrong answer four times in this engine. @@ -125,14 +142,12 @@ element_type_of(p, src, t) :- // self-only, here the ELEMENT type of a container held on an attribute is. The self // clauses stay as the special case; nothing about what an element type means changes. element_type_of(p, src, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_declared_element(p, f, t). element_type_of(p, src, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_element_from_param(p, f, t). @@ -278,6 +293,79 @@ binding_element_type(p, b, t) :- expr_binding(p, b, _, tgt), element_type_of(p, src, t). +// ── A MAPPING'S VIEWS: `.values()`, `.keys()`, `.items()` ───────────────────── +// self.dump_fields: dict[str, Field] = {} +// for name, field_obj in self.dump_fields.items(): field_obj.serialize(...) +// Every clause above types the container itself; a view is a CALL on it, and none of them +// looked through one, so the commonest way to walk a mapping typed nothing -- and an +// `items()` loop binds a TUPLE target, which no clause bound at all. A mapping annotation +// names its key at generic position 0 and its value at position 1, so the slots are +// read positionally rather than through type_ref_element (which merges both, the safe +// direction for iterating the mapping itself, and wrong for a view that yields one side). +// view_slot names which side each view yields; `items()` yields (key, value) pairs. +// dict_view_slot / dict_items_view are CPython names, catalogued in resolution/builtins.dl. + +// the annotation a mapping-valued expression carries: a field (through self or through +// any typed object), a parameter, or a callee's declared return +mapping_annotation_of(src, r) :- + expr_attr_of_self("client", src, n, owner), + type_attr_field("client", owner, n, f), + type_ref_owner("client", f, "FIELD", r), + type_ref("client", k, "FIELD_TYPE", _, _, r), annotation_container_kind(k). +mapping_annotation_of(src, r) :- + attr_access("client", src, n, obj), + expr_type("client", obj, ot), + type_attr_field("client", ot, n, f), + type_ref_owner("client", f, "FIELD", r), + type_ref("client", k, "FIELD_TYPE", _, _, r), annotation_container_kind(k). +mapping_annotation_of(src, r) :- + expr_names_param("client", src, ph), + type_ref_owner("client", ph, "METHOD_PARAM", r), + type_ref("client", k, "METHOD_PARAM", _, _, r), annotation_container_kind(k). +mapping_annotation_of(src, r) :- + call_of_expr(src, site), expr_call_candidate(site, m), + type_ref_owner("client", m, "METHOD", r), + type_ref("client", k, "METHOD_RETURN", _, _, r), annotation_container_kind(k). +// a single-write local holding one of those +mapping_annotation_of(src, r) :- + expr_binding("client", b, ctx, src), ctx != "STORE", + binding_lookup("client", b, b2), + binding_rebinding("client", "1", _, _, b2), + expr_binding("client", b2, "STORE", tgt), + assign_pair("client", tgt, val), + mapping_annotation_of(val, r). + +// the type at one generic position of an annotation (resolved, or by name in its module) +type_ref_slot(r, pos, t) :- + type_ref_nesting("client", r, pos, "1", child), + type_ref("client", _, "GENERIC_ARGUMENT", _, _, child), + type_ref_resolved("client", t, child). +type_ref_slot(r, pos, t) :- + type_ref_nesting("client", r, pos, "1", child), + type_ref("client", _, "GENERIC_ARGUMENT", tn, _, child), tn != "", + type_ref_owner("client", owner, ok, r), + annotation_owner_module("client", owner, ok, mod), + type_name_in_module("client", mod, tn, t). + +// `m.keys()` / `m.values()` / `m.items()` written with no argument +dict_view_call(src, recv, view) :- + call_of_expr(src, site), call_name(site, view), dict_view_name(view), + call_receiver_object(site, recv), !call_arg(site, _, _). +// the element a `keys()` / `values()` view yields +element_type_of("client", src, t) :- + dict_view_call(src, recv, view), dict_view_slot(view, pos), + mapping_annotation_of(recv, r), r != "", type_ref_slot(r, pos, t). +// an `items()` pair's two slots, and the names a TUPLE loop target binds to them +dict_item_slot_type(src, pos, t) :- + dict_items_view(view), dict_view_call(src, recv, view), dict_view_slot(_, pos), + mapping_annotation_of(recv, r), type_ref_slot(r, pos, t). +binding_element_type("client", b, t) :- + iter_pair("client", tgt, src), + expr_node("client", "TUPLE", _, _, tgt), + expr_parent("client", tgt, "ELEMENT", pos, el), + expr_binding("client", b, _, el), + dict_item_slot_type(src, pos, t). + // ── iteration_untyped(Prov, TargetExprHash, IterableExprHash) ──────────────── // A loop whose container the engine could not type. Emitted so the cost is countable // rather than absorbed into a general "receiver untyped". diff --git a/graph/python/engine/resolution/name-resolution.dl b/graph/python/engine/resolution/name-resolution.dl index 33df19ae..b9345eb1 100644 --- a/graph/python/engine/resolution/name-resolution.dl +++ b/graph/python/engine/resolution/name-resolution.dl @@ -161,13 +161,20 @@ assign_pair(p, te, ve) :- // the two-member set is the correct answer. A block that contains exactly one of the // two is what proves the separation, and py_block — emitted by the parser and read by // no rule before this — is the only place that is written down. +// RESTRICTED TO THE PAIRS ITS ONE CONSUMER ASKS ABOUT. def_rebound_by_later_def reads +// this only for two defs of the SAME BINDING; materialized over every same-file pair it +// held 1.88M rows (230s) on a 526k-LOC subject, of which the consumer could ever touch a +// few hundred. The binding join also keys the method pairs, so the block scan runs per +// rebinding candidate instead of per file-wide cross product. Queried results unchanged. def_separated_by_block(p, m1, m2) :- + method_declaring_binding(p, b, m1), method_declaring_binding(p, b, m2), method_file(p, f, m1), method_file(p, f, m2), method_start_line(p, l1s, m1), method_start_line(p, l2s, m2), block_span(p, f, bs, be, _), l1 = to_number(l1s), l2 = to_number(l2s), bstart = to_number(bs), bend = to_number(be), l1 >= bstart, l1 <= bend, l2 > bend. def_separated_by_block(p, m1, m2) :- + method_declaring_binding(p, b, m1), method_declaring_binding(p, b, m2), method_file(p, f, m1), method_file(p, f, m2), method_start_line(p, l1s, m1), method_start_line(p, l2s, m2), block_span(p, f, bs, be, _), @@ -1001,18 +1008,41 @@ computed_name_prefix(nameArg, pre) :- expr_operator("client", op, nameArg), py_str_concat_operator(op), expr_parent("client", nameArg, "OPERAND_LEFT", _, l), expr_node("client", "LITERAL", _, pre, l), pre != "". +// The same prefix written as an f-string, `getattr(self, f"visit_{type(node).__name__}")`: +// the parser hands over an f-string's constant lead (the text before its first `{`) as its +// literal value, and every string the f-string can produce starts with it. +computed_name_prefix(nameArg, pre) :- + expr_node("client", "FSTRING", _, pre, nameArg), pre != "". getattr_computed_name(e, obj, pre) :- call_of_expr(e, site), call_name(site, "getattr"), call_arg(site, "0", obj), call_arg(site, "1", nameArg), computed_name_prefix(nameArg, pre). -expr_denotes_method("client", e, m) :- +// The receiver's type, and where the receiver is `self`, every CONSTRUCTED subclass of it too: +// a visitor base names `visit_*` members it does not define itself, and self is whichever +// subclass was built -- the same self-dispatch bound (RTA) dispatch.dl puts on `self.m()`. +getattr_computed_type(e, pre, t) :- getattr_computed_name(e, obj, pre), - expr_type("client", obj, t), + expr_type("client", obj, t). +getattr_computed_type(e, pre, s) :- + getattr_computed_name(e, obj, pre), + expr_node("client", "SELF_REFERENCE", _, _, obj), + expr_type("client", obj, k), + type_subclass("client", k, s), + type_instantiated_reachable("client", s). +getattr_computed_member(e, m) :- + getattr_computed_type(e, pre, t), mro_lookup("client", t, n, m), strlen(n) > strlen(pre), substr(n, 0, strlen(pre)) = pre. +// a member a decorator replaced is the callable the decorator returned (a wrapper) +getattr_computed_member(e, m) :- + getattr_computed_type(e, pre, t), + mro_lookup_decorated("client", t, n, m), + strlen(n) > strlen(pre), + substr(n, 0, strlen(pre)) = pre. +expr_denotes_method("client", e, m) :- getattr_computed_member(e, m). // ── getattr(obj, name, default) IS AT LEAST the default ────────────────────── // h = getattr(record, "highlighter", self.highlighter) diff --git a/graph/python/engine/resolution/value-flow.dl b/graph/python/engine/resolution/value-flow.dl index 230c1f1b..6fd224af 100644 --- a/graph/python/engine/resolution/value-flow.dl +++ b/graph/python/engine/resolution/value-flow.dl @@ -97,6 +97,52 @@ param_arg_type(ph, t) :- param_decl("client", kw, _, _, m, ph), expr_type("client", a, t). +// ── a parameter the TEST RUNNER fills ───────────────────────────────────────── +// pytest calls `test_commit(session=)`: the call is the runner's, +// so no call site spells it, but it is an argument reaching a parameter all the same, and +// the parameter is typed exactly as one passed at a call site would be. Which fixture +// serves the parameter is framework-behavior/dispatch.dl's py_fixs_injection (2a), the +// runner's own lookup (same class, same module, nearest conftest, a pytest_plugins +// module), so this is as deterministic as a name the LEGB walk resolves. +// MEASURED across ten public suites: 1,529 method calls on such parameters were +// ambiguous_unknown, and everything derived from them (`tx = session.begin(); tx.commit()`) +// with them; a suite whose fixtures carry annotations resolved its own. +param_arg_type(ph, t) :- + py_fixs_injection(req, fix, pn), + param_decl("client", pn, _, _, req, ph), + py_fixture_value_type(fix, t). + +// ── a parameter's DEFAULT VALUE reaches it ──────────────────────────────────── +// `def getter(val, field_obj=field_obj): return field_obj.deserialize(val)` -- the closure +// idiom that pins a loop variable -- is called with one argument, so the only value +// field_obj ever holds is its default, and no call site passes one. The default is an +// argument the def statement writes once, evaluated where the def runs: one more value +// reaching the parameter, alongside whatever the call sites pass. +param_arg_type(ph, t) :- param_default_expr("client", e, ph), expr_type("client", e, t). +param_arg_class(ph, t) :- param_default_expr("client", e, ph), expr_type_class_object("client", e, t). +param_arg_method(ph, m) :- param_default_expr("client", e, ph), expr_denotes_method("client", e, m). + +// ── a parameter the runner fills from a PARAMETRIZE row, or from FIXTURE PARAMS ── +// `@pytest.mark.parametrize("cls", [String, Integer])` calls the test once per value, so +// each value reaches `cls` as an argument would: an instance types it, a class object +// makes it that class, a function makes it callable as that function. Inside a fixture +// declared `params=[...]`, `request.param` is each of those values in turn. +// framework-behavior/dispatch.dl reads the rows (py_pz_param_value, py_pz_request_param). +param_arg_type(ph, t) :- py_pz_param_value(ph, v), expr_type("client", v, t). +param_arg_class(ph, t) :- py_pz_param_value(ph, v), expr_type_class_object("client", v, t). +param_arg_method(ph, m) :- py_pz_param_value(ph, v), expr_denotes_method("client", v, m). +expr_type("client", e, t) :- py_pz_request_param(e, v), expr_type("client", v, t). +expr_type_class_object("client", e, t) :- py_pz_request_param(e, v), expr_type_class_object("client", v, t). +expr_denotes_method("client", e, m) :- py_pz_request_param(e, v), expr_denotes_method("client", v, m). +// a fixture that hands back a CLASS (`return request.param` over classes) makes the +// requesting parameter that class, as a returned instance makes it an instance +py_fixture_value_class(fix, t) :- py_fixture_decl(fix, _), + method_return_value_expr("client", fix, e), expr_type_class_object("client", e, t). +param_arg_class(ph, t) :- + py_fixs_injection(req, fix, pn), + param_decl("client", pn, _, _, req, ph), + py_fixture_value_class(fix, t). + // ── param_arg_method(ParamHash, MethodHash) — a CALLABLE reaching a parameter ── // `Delegator(target)` puts the FUNCTION `target` into the parameter `fn`. Java would // need a functional interface for this; in Python it is an ordinary assignment. @@ -230,9 +276,8 @@ type_attr_class_value(p, t, n, c) :- // never a single edge to the assigned value. `self.x = ...` is left to the field rules, // which see it with its write count. instance_attr_method_value(p, t, n, m) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", + attr_access(p, e, n, obj), expr_name_context(p, "STORE", e), - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), !expr_node(p, "SELF_REFERENCE", _, _, obj), expr_type(p, obj, t), assign_pair(p, e, val), @@ -242,10 +287,9 @@ instance_attr_method_value(p, t, n, m) :- // denotes what was assigned to it, beside the bound class method expr_denotes_bound_method // already gives it. expr_denotes_method(p, e, m) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", + attr_access(p, e, n, obj), !expr_name_context(p, "STORE", e), !call_callee_expr(_, e), - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), expr_type(p, obj, t), instance_attr_method_value(p, t, n, m). @@ -260,9 +304,7 @@ method_dispatch_candidate(base, m, "value") :- // Read as a value -- `k = self._row_class` -- the attribute denotes the class, so a // call on it or through it resolves like any other class object. expr_type_class_object(p, e, c) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", - !expr_name_context(p, "STORE", e), - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, e, n, obj), !expr_name_context(p, "STORE", e), expr_type(p, obj, t), type_attr_class_value(p, t, n, c). diff --git a/graph/python/libsum/libsum.py b/graph/python/libsum/libsum.py new file mode 100644 index 00000000..304a8cf5 --- /dev/null +++ b/graph/python/libsum/libsum.py @@ -0,0 +1,1127 @@ +#!/usr/bin/env python3 +"""libsum.py -- what an installed Python library does with the objects a client hands it. + + libsum.py --src --out [--site

[,...]] [--cache ] + +THE GAP THIS CLOSES. The engine reads a dependency's signatures, never its bodies, so a hop the +LIBRARY makes back into client code is invisible: a test client's `get()` calls the `open()` a +client subclass overrides, a CLI runner calls `main()` on the command it was handed, a test client +built around an application calls `app(environ, start_response)` on it. Every one of those is a +library frame between two client frames, and the call graph stops at the first. + +WHAT A SUMMARY SAYS. One row per hand-back a library callable is known to make, rooted at one of +its parameters (or at `self`, the object it was called on): + + py-lib-callback.csv callee root pos path member via type + callee the library callable, canonical dotted name: `pkg.mod.func`, `pkg.mod.Class.method` + (one row per method a class HAS, its inherited ones included), `pkg.mod.Class` for + constructing it + root the parameter the object arrives in, or `self` + pos where the CLIENT writes that argument: the index among the positional arguments of + the call as written (an instance method's `self` is not written), -1 for `self` + path the attribute chain the library follows from that object first ("" for the object + itself, `application` for `self.application`), at most two long + member the method the library invokes on what it reached: a name it calls, or the dunder a + syntax runs (`__call__` for `x()`, `__enter__` for `with x`, `__iter__` for `for`...) + via the library function in which that invocation is written + type the class the LIBRARY declares that object to be (a parameter annotation, a field's), "" when it + declares none: when the client's own object cannot be told, a client subclass of it is the answer + py-lib-field-init.csv class field param pos + constructing `class` stores the argument `param` (written at `pos`) on the new object as + `field`, so a later `self.` hand-back reaches what the client passed + py-lib-ancestor.csv class ancestor + a library class's library ancestors, for the classes the client can name + py-lib-alias.csv alias canonical + a name a package re-exports (`clikit.Command` is `clikit.core.Command`) + py-lib-decorates.csv decorator form class field + applying the library callable as a decorator -- `@decorator` (DIRECT) or `@decorator(...)` (FACTORY) -- + to a function f yields a new `class` whose `field` holds f; class "" when f itself comes back + +HOW, AND WHY NOT IN THE SOLVE. Library source is just Python, so the facts are DERIVED from it by a +small flow-insensitive pass over each function body (which parameter-rooted value meets which call or +protocol syntax), composed to a fixpoint across the library's own calls: `Client.get` calls +`self.open`, `open` calls `run_app(self.application, ...)`, `run_app` calls `app(...)`, so +`Client.get` hands back `__call__` on `self.application`. Nothing is solved over library bodies at +index time: the pass reads only the packages the client imports (and what they import), is cached by +package and version, and writes three small tables. A library written in C has no body to read and +yields nothing; that is a declared blind spot, not a guess. + +WHERE THE LIBRARY IS. The project's own virtual environment (`.venv`, `venv`, `env` in the repository, +else $VIRTUAL_ENV), its stdlib through pyvenv.cfg, or AXIOMCODE_PY_SITE (a comma list of directories) +when set. No environment found means no summaries: the engine then behaves exactly as before. +""" +import argparse, ast, csv, hashlib, json, os, re, sys + +VERSION = '1' +MAXPATH = 2 +DUNDER_BUILTIN = {'len': '__len__', 'str': '__str__', 'repr': '__repr__', 'iter': '__iter__', 'next': '__next__', + 'hash': '__hash__', 'bool': '__bool__', 'format': '__format__', 'reversed': '__reversed__', + 'abs': '__abs__', 'aiter': '__aiter__', 'anext': '__anext__'} +SKIP_DIRS = {'__pycache__', 'tests', 'test', 'testing_support', 'docs', 'examples', 'benchmarks'} + + +# ───────────────────────────────────────────────────────────────────────────── +# where the library is +# ───────────────────────────────────────────────────────────────────────────── +def find_sites(src): + if os.environ.get('AXIOMCODE_PY_SITE'): + return [d for d in os.environ['AXIOMCODE_PY_SITE'].split(',') if os.path.isdir(d)], None + envs = [os.path.join(src, n) for n in ('.venv', 'venv', 'env', '.env')] + if os.environ.get('VIRTUAL_ENV'): + envs.append(os.environ['VIRTUAL_ENV']) + for e in envs: + cfg = os.path.join(e, 'pyvenv.cfg') + if not os.path.isfile(cfg): + continue + sites = [] + for root in (os.path.join(e, 'lib'), os.path.join(e, 'Lib')): + if not os.path.isdir(root): + continue + if os.path.isdir(os.path.join(root, 'site-packages')): + sites.append(os.path.join(root, 'site-packages')) + for d in sorted(os.listdir(root)): + sp = os.path.join(root, d, 'site-packages') + if d.startswith('python') and os.path.isdir(sp): + sites.append(sp) + stdlib = None + try: + kv = dict(l.split('=', 1) for l in open(cfg, encoding='utf-8') if '=' in l) + kv = {k.strip(): v.strip() for k, v in kv.items()} + home, ver = kv.get('home', ''), (kv.get('version_info') or kv.get('version') or '') + mm = '.'.join(ver.split('.')[:2]) + for cand in (os.path.join(os.path.dirname(home), 'lib', f'python{mm}'), os.path.join(home, 'Lib'), + os.path.join(os.path.dirname(home), 'Lib')): + if mm and os.path.isfile(os.path.join(cand, 'contextlib.py')): + stdlib = cand; break + except Exception: + pass + if sites: + return sites, stdlib + return [], None + + +# ───────────────────────────────────────────────────────────────────────────── +# which modules: the client's imports, then what those import, transitively +# ───────────────────────────────────────────────────────────────────────────── +IMPORT_RE = re.compile(r'^\s*(?:from\s+([A-Za-z_][\w.]*)\s+import|import\s+([A-Za-z_][\w.]*(?:\s*,\s*[A-Za-z_][\w.]*)*))', re.M) + + +def client_imports(src, ir): + """every dotted path the client imports: `from a.b import C` gives a.b.C, `import a.b` gives a.b. From the + parser's import table when the IR is there, else from the source""" + paths = set() + p = os.path.join(ir, 'all-python-imports.csv') if ir else '' + if p and os.path.isfile(p): + try: + with open(p, newline='', encoding='utf-8') as fh: + r = csv.reader(fh, delimiter='\t') + head = next(r) + if 'importedPath' in head: + col = head.index('importedPath') + for row in r: + if len(row) > col and row[col]: + paths.add(row[col]) + return paths + except Exception: + pass + skip = {'.venv', 'venv', 'env', '.env', '.git', 'node_modules', '.tox', '__pycache__', '.axiomcode'} + for d, ds, fs in os.walk(src): + ds[:] = [x for x in ds if x not in skip] + for f in fs: + if not f.endswith('.py'): + continue + try: + tree = ast.parse(open(os.path.join(d, f), encoding='utf-8', errors='replace').read()) + except Exception: + continue + for n in ast.walk(tree): + if isinstance(n, ast.Import): + paths.update(a.name for a in n.names) + elif isinstance(n, ast.ImportFrom) and n.module and not n.level: + paths.update(f'{n.module}.{a.name}' for a in n.names if a.name != '*') + paths.add(n.module) + return paths + + +class Package: + """one importable top-level name: a directory package or a single module file""" + def __init__(self, name, path, base): + self.name, self.path, self.base = name, path, base + + def files(self): + if os.path.isfile(self.path): + yield self.name, self.path + return + for d, ds, fs in os.walk(self.path): + ds[:] = sorted(x for x in ds if x not in SKIP_DIRS and not x.startswith('.')) + for f in sorted(fs): + if not f.endswith('.py'): + continue + rel = os.path.relpath(os.path.join(d, f), self.base)[:-3].replace(os.sep, '.') + if rel.endswith('.__init__'): + rel = rel[:-9] + yield rel, os.path.join(d, f) + + +def locate(name, sites, stdlib): + for base in list(sites) + ([stdlib] if stdlib else []): + p = os.path.join(base, name) + if os.path.isdir(p) and (os.path.isfile(os.path.join(p, '__init__.py')) or base != stdlib): + return Package(name, p, base) + if os.path.isfile(p + '.py'): + return Package(name, p + '.py', base) + return None + + +# ───────────────────────────────────────────────────────────────────────────── +# one module: its namespace, its classes, and the events in each function body +# ───────────────────────────────────────────────────────────────────────────── +class Fn: + __slots__ = ('q', 'params', 'pos', 'kind', 'events', 'cls', 'stores', 'ptypes', 'rets', 'local') + + def __init__(self, q, cls): + self.q, self.cls, self.ptypes, self.rets, self.local = q, cls, {}, [], False + self.params, self.pos, self.kind = [], {}, 'function' + self.events = [] # ('use', val, member) | ('call', callee, selfval, {param: [vals]}, [posvals], {kw: [vals]}) + self.stores = [] # (field, val) written on self + + +class Cls: + __slots__ = ('q', 'bases', 'methods', 'mod', 'ftypes') + + def __init__(self, q, mod): + self.q, self.mod, self.bases, self.methods, self.ftypes = q, mod, [], {}, {} + + +def resolve_rel(mod, is_pkg, level, name): + parts = mod.split('.') + if not is_pkg: + parts = parts[:-1] + if level > 1: + parts = parts[:len(parts) - (level - 1)] + return '.'.join([p for p in parts if p] + ([name] if name else [])) + + +class Module: + def __init__(self, name, path, is_pkg): + self.name, self.path, self.is_pkg = name, path, is_pkg + self.ns = {} # local name -> qualified name (a def, a class, an imported thing) + self.fns, self.classes = {}, {} + self.imports = set() + self.stars, self.all_names = [], None # `from M import *` sources; a literal __all__, when declared + self.call_alias = {} # `Name = wrap(pkg.Class, ...)`: the dotted first argument, an alias when it is a class + + def load(self): + try: + tree = ast.parse(open(self.path, encoding='utf-8', errors='replace').read()) + except Exception: + return False + self.tree = tree + for st in tree.body: + self._ns_stmt(st) + return True + + def _ns_stmt(self, st): + if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + self.ns[st.name] = f'{self.name}.{st.name}' + elif isinstance(st, ast.ImportFrom): + base = resolve_rel(self.name, self.is_pkg, st.level, st.module or '') if st.level else (st.module or '') + if base: + self.imports.add(base) + for a in st.names: + if a.name != '*': + self.ns[a.asname or a.name] = f'{base}.{a.name}' if base else a.name + elif base: + self.stars.append(base) + elif isinstance(st, ast.Import): + for a in st.names: + self.imports.add(a.name) + if a.asname: + self.ns[a.asname] = a.name + else: + self.ns.setdefault(a.name.split('.')[0], a.name.split('.')[0]) + elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name) \ + and st.targets[0].id == '__all__' and isinstance(st.value, (ast.List, ast.Tuple)): + self.all_names = [e.value for e in st.value.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)] + elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name) \ + and isinstance(st.value, ast.Call) and st.value.args and dotted(st.value.args[0]): + v = dotted(st.value.args[0]) + if v.split('.')[0] in self.ns: + self.call_alias.setdefault(st.targets[0].id, self.ns[v.split('.')[0]] + v[len(v.split('.')[0]):]) + elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name): + v = dotted(st.value) + if v and v.split('.')[0] in self.ns: + self.ns.setdefault(st.targets[0].id, self.ns[v.split('.')[0]] + v[len(v.split('.')[0]):]) + elif isinstance(st, (ast.If, ast.Try)): + for b in [st.body, getattr(st, 'orelse', []), getattr(st, 'finalbody', [])] + [h.body for h in getattr(st, 'handlers', [])]: + for s in b: + self._ns_stmt(s) + + def qual(self, n): + return self.ns.get(n) + + def analyse(self): + for st in self.tree.body: + if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef)): + self._fn(st, None) + elif isinstance(st, ast.ClassDef): + c = Cls(f'{self.name}.{st.name}', self) + c.bases = [b for b in (self.qual_expr(x) for x in st.bases) if b] + self.classes[c.q] = c + for s in st.body: + if isinstance(s, ast.AnnAssign) and isinstance(s.target, ast.Name): + t = self.ann_qual(s.annotation) + if t: + c.ftypes[s.target.id] = t + for s in st.body: + if isinstance(s, (ast.FunctionDef, ast.AsyncFunctionDef)): + f = self._fn(s, c) + c.methods[s.name] = f.q + + def ann_qual(self, ann): + """the class an annotation names: `T`, `pkg.T`, `"T"`, `Optional[T]`, `T | None`""" + if isinstance(ann, ast.Constant) and isinstance(ann.value, str): + try: + ann = ast.parse(ann.value, mode='eval').body + except SyntaxError: + return None + if isinstance(ann, ast.BinOp): + return self.ann_qual(ann.left) + if isinstance(ann, ast.Subscript) and (dotted(ann.value) or '').split('.')[-1] == 'Optional': + return self.ann_qual(ann.slice) + return self.qual_expr(ann) if ann is not None else None + + def qual_expr(self, e): + d = dotted(e.value if isinstance(e, ast.Subscript) else e) + if not d: + return None + head, _, rest = d.partition('.') + q = self.qual(head) + return (q + ('.' + rest if rest else '')) if q else None + + def _fn(self, node, cls): + q = f'{cls.q}.{node.name}' if cls else f'{self.name}.{node.name}' + f = self.signature(node, q, cls) + self.fns[q] = f + Body(self, f, f.params[0] if f.kind in ('method', 'classmethod', 'property') and f.params else None, cls).run(node) + return f + + def signature(self, node, q, cls): + f = Fn(q, cls.q if cls else None) + decos = {dotted(d.func if isinstance(d, ast.Call) else d) for d in node.decorator_list} + a = node.args + names = [x.arg for x in a.posonlyargs + a.args] + if cls is not None: + if 'staticmethod' in decos: + f.kind = 'static' + elif 'classmethod' in decos: + f.kind = 'classmethod' + elif 'property' in decos or any(d and d.endswith(('.setter', '.getter', 'cached_property')) for d in decos): + f.kind = 'property' + else: + f.kind = 'method' + f.params = names + [x.arg for x in a.kwonlyargs] + skip = 1 if f.kind in ('method', 'classmethod', 'property') and names else 0 + for i, n in enumerate(names): + f.pos[n] = i - skip if i >= skip else -1 + for x in a.kwonlyargs: + f.pos[x.arg] = -2 # keyword only: matched by name + for x in a.posonlyargs + a.args + a.kwonlyargs: + if x.annotation is not None: + t = self.ann_qual(x.annotation) + if t: + f.ptypes[x.arg] = t + return f + + +def dotted(e): + if isinstance(e, ast.Name): + return e.id + if isinstance(e, ast.Attribute): + d = dotted(e.value) + return f'{d}.{e.attr}' if d else None + return None + + +class Body: + """a flow-insensitive pass over one function body (nested defs and lambdas included: a closure the + library builds around a parameter is how a decorator or a callback wrapper hands it back)""" + def __init__(self, mod, fn, selfname, cls): + self.mod, self.fn, self.selfname, self.cls = mod, fn, selfname, cls + self.env = {} + self.params = set(fn.params) + + def run(self, node): + for _ in range(2): # twice: a local assigned below its first use + self.fn.events = []; self.fn.stores = []; self.fn.rets = [] + for st in node.body: + self.stmt(st) + return self + + # values: ('p', root, path) | ('ref', qname) | ('new', class, argvals) | ('self', ...) is ('p', 'self', ()) + def val(self, e): + if isinstance(e, ast.Name): + if e.id in self.env: + return self.env[e.id] + if e.id in self.params: + return [('p', 'self' if e.id == self.selfname else e.id, ())] + q = self.mod.qual(e.id) + return [('ref', q)] if q else [] + if isinstance(e, ast.Attribute): + out = [] + for v in self.val(e.value): + if v[0] == 'p' and len(v[2]) < MAXPATH: + out.append(('p', v[1], v[2] + (e.attr,))) + elif v[0] == 'ref': + out.append(('ref', f'{v[1]}.{e.attr}')) + elif v[0] == 'new': + out.append(('field', v, e.attr)) + return out + if isinstance(e, ast.Call): + return self.call(e) + if isinstance(e, (ast.IfExp,)): + self.val(e.test) + return self.val(e.body) + self.val(e.orelse) + if isinstance(e, ast.BoolOp): + out = [] + for x in e.values: + out += self.val(x) + return out + if isinstance(e, ast.NamedExpr): + v = self.val(e.value) + if isinstance(e.target, ast.Name): + self.env[e.target.id] = v + return v + if isinstance(e, ast.Await): + return self.val(e.value) + if isinstance(e, ast.Subscript): + for v in self.val(e.value): + self.use(v, '__getitem__') + self.val(e.slice) + return [] + if isinstance(e, ast.Compare): + self.val(e.left) + for op, c in zip(e.ops, e.comparators): + cv = self.val(c) + if isinstance(op, (ast.In, ast.NotIn)): + for v in cv: + self.use(v, '__contains__') + return [] + if isinstance(e, ast.JoinedStr): + for x in e.values: + if isinstance(x, ast.FormattedValue): + m = {115: '__str__', 114: '__repr__', 97: '__repr__'}.get(x.conversion, '__format__') + for v in self.val(x.value): + self.use(v, m) + return [] + if isinstance(e, ast.Lambda): + self.nested(e.body if isinstance(e.body, list) else [ast.Expr(e.body)], e.args) + return [] + if isinstance(e, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)): + for g in e.generators: + for v in self.val(g.iter): + self.use(v, '__iter__') + for c in g.ifs: + self.val(c) + for x in ([e.key, e.value] if isinstance(e, ast.DictComp) else [e.elt]): + self.val(x) + return [] + for ch in ast.iter_child_nodes(e): + if isinstance(ch, ast.expr): + self.val(ch) + return [] + + def use(self, v, member): + if member == '__call__' and v == ('p', 'self', ()) and self.fn.kind == 'classmethod': + member = '__init__' # cls(...) constructs the class the method was called on + if v[0] == 'p' or v[0] == 'field': + # what the library itself declares the object to be, where it says: a parameter's annotation, a field's + hint = None + if v[0] == 'p': + base = self.cls.q if v[1] == 'self' and self.cls else self.fn.ptypes.get(v[1]) + if base and not v[2]: + hint = ('t', base) + elif base and len(v[2]) == 1: + hint = ('f', base, v[2][0]) + self.fn.events.append(('use', v, member, hint)) + + def call(self, e): + f = e.func + posv = [self.val(a) if not isinstance(a, ast.Starred) else None for a in e.args] + kwv = {k.arg: self.val(k.value) for k in e.keywords if k.arg} + for k in e.keywords: + if not k.arg: + self.val(k.value) + if isinstance(f, ast.Name) and f.id in DUNDER_BUILTIN and f.id not in self.env and not self.mod.qual(f.id) and posv and posv[0]: + for v in posv[0]: + self.use(v, DUNDER_BUILTIN[f.id]) + return [] + if isinstance(f, ast.Name) and f.id == 'super' and not e.args: + return [('super',)] + fd = dotted(f) or '' + if fd.split('.')[-1] == 'cast' and len(posv) == 2 and posv[1]: + return posv[1] # typing.cast(T, x) is x + if fd == 'type' and len(posv) == 1 and posv[0]: + return posv[0] # type(x).__enter__ is looked up on x's class: x's member + if isinstance(f, ast.Attribute): + out = [] + for rv in self.val(f.value): + if rv[0] in ('p', 'field'): + self.use(rv, f.attr) + self.fn.events.append(('call', ('method', rv, f.attr), posv, kwv)) + elif rv[0] == 'super': + self.fn.events.append(('call', ('super', self.cls.q if self.cls else None, f.attr), posv, kwv)) + elif rv[0] == 'new': + self.fn.events.append(('call', ('method', rv, f.attr), posv, kwv)) + elif rv[0] == 'ref': + self.fn.events.append(('call', ('ref', f'{rv[1]}.{f.attr}'), posv, kwv)) + return out + out = [] + for cv in self.val(f): + if cv[0] in ('p', 'field'): + self.use(cv, '__call__') + if cv[0] == 'p' and cv[2]: + # a member read first and called later (`_enter = type(cm).__enter__; _enter(cm)`) + self.use(('p', cv[1], cv[2][:-1]), cv[2][-1]) + elif cv[0] == 'ref': + self.fn.events.append(('call', ('ref', cv[1]), posv, kwv)) + out.append(('new', cv[1], self.args(posv, kwv))) + elif cv[0] == 'closure': + self.fn.events.append(('call', ('ref', cv[1]), posv, kwv)) + out.append(('res', cv[1], self.args(posv, kwv))) + elif cv[0] in ('new', 'res', 'res2'): + out.append(('res2', cv, self.args(posv, kwv))) # calling what a call returned: `command(...)(f)` + return out + + @staticmethod + def args(posv, kwv): + return (tuple(tuple(p) if p is not None else None for p in posv), tuple(sorted((k, tuple(v)) for k, v in kwv.items()))) + + def nested(self, body, args): + saved = set(self.params) + for a in args.posonlyargs + args.args + args.kwonlyargs + [x for x in (args.vararg, args.kwarg) if x]: + self.params.discard(a.arg); self.env.pop(a.arg, None) + for st in body: + self.stmt(st) + self.params = saved + + def assign(self, t, vals): + if isinstance(t, ast.Name): + # flow-insensitive: a parameter rebound keeps what it arrived as (`callback = other.callback`) + base = self.env.get(t.id) or ([('p', 'self' if t.id == self.selfname else t.id, ())] if t.id in self.params else []) + self.env[t.id] = list(dict.fromkeys(base + vals)) + elif isinstance(t, ast.Attribute): + for ov in self.val(t.value): + if ov == ('p', 'self', ()): + for v in vals: + self.fn.stores.append((t.attr, v)) + if self.cls and v[0] == 'p' and not v[2] and v[1] in self.fn.ptypes: + self.cls.ftypes.setdefault(t.attr, self.fn.ptypes[v[1]]) + elif isinstance(t, (ast.Tuple, ast.List)): + for x in t.elts: + self.assign(x, []) + elif isinstance(t, ast.Subscript): + for v in self.val(t.value): + self.use(v, '__setitem__') + + def stmt(self, st): + if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef)): + self.nested(st.body, st.args) + # and as a callable of its own, so what it RETURNS is known: a decorator factory returns this closure, + # and the closure returns the object it wrapped the decorated function in + sq = f'{self.fn.q}..{st.name}' + sub = self.mod.signature(st, sq, None) + sub.local = True + self.mod.fns[sq] = sub + b = Body(self.mod, sub, None, None) + b.env = {k: list(v) for k, v in self.env.items() if k not in set(sub.params)} + b.run(st) + self.env[st.name] = [('closure', sq)] + return + if isinstance(st, ast.Return): + if st.value is not None: + self.fn.rets.extend(self.val(st.value)) + return + if isinstance(st, ast.ClassDef): + return + if isinstance(st, ast.Assign): + v = self.val(st.value) + for t in st.targets: + self.assign(t, v) + return + if isinstance(st, ast.AnnAssign) and isinstance(st.target, ast.Attribute) and self.cls \ + and isinstance(st.target.value, ast.Name) and st.target.value.id == self.selfname: + t = self.mod.ann_qual(st.annotation) + if t: + self.cls.ftypes[st.target.attr] = t + if isinstance(st, (ast.AnnAssign, ast.AugAssign)): + if st.value is not None: + v = self.val(st.value) + self.assign(st.target, v if isinstance(st, ast.AnnAssign) else []) + return + if isinstance(st, (ast.For, ast.AsyncFor)): + for v in self.val(st.iter): + self.use(v, '__aiter__' if isinstance(st, ast.AsyncFor) else '__iter__') + self.assign(st.target, []) + if isinstance(st, (ast.With, ast.AsyncWith)): + a = isinstance(st, ast.AsyncWith) + for it in st.items: + vs = self.val(it.context_expr) + for v in vs: + self.use(v, '__aenter__' if a else '__enter__'); self.use(v, '__aexit__' if a else '__exit__') + if it.optional_vars is not None: + self.assign(it.optional_vars, []) + if isinstance(st, ast.Delete): + for t in st.targets: + if isinstance(t, ast.Subscript): + for v in self.val(t.value): + self.use(v, '__delitem__') + return + for fld, ch in ast.iter_fields(st): + if fld in ('target', 'targets', 'iter', 'items') and isinstance(st, (ast.For, ast.AsyncFor, ast.With, ast.AsyncWith)): + continue + for c in (ch if isinstance(ch, list) else [ch]): + if isinstance(c, ast.stmt): + self.stmt(c) + elif isinstance(c, ast.expr): + self.val(c) + elif isinstance(c, ast.excepthandler): + for s in c.body: + self.stmt(s) + elif isinstance(c, ast.match_case) if hasattr(ast, 'match_case') else False: + for s in c.body: + self.stmt(s) + + +# ───────────────────────────────────────────────────────────────────────────── +# the program: every analysed module, then the fixpoint across the library's own calls +# ───────────────────────────────────────────────────────────────────────────── +class Program: + def __init__(self): + self.mods, self.fns, self.classes = {}, {}, {} + + def add(self, m): + self.mods[m.name] = m + self.fns.update(m.fns); self.classes.update(m.classes) + + def canon(self, q, seen=None): + """follow a module re-export to the declaration: clikit.Command -> clikit.core.Command""" + if q in self.fns or q in self.classes: + return q + seen = seen or set() + if q in seen or not q: + return None + seen.add(q) + parts = q.split('.') + for i in range(len(parts) - 1, 0, -1): + m = self.mods.get('.'.join(parts[:i])) + if m is None: + continue + nxt = m.qual(parts[i]) + if nxt and nxt != '.'.join(parts[:i + 1]): + return self.canon('.'.join([nxt] + parts[i + 1:]), seen) + if not nxt and parts[i] in m.call_alias: + # a generic alias built around a class (`Mapping = _alias(collections.abc.Mapping, 2)`) is that class; + # a wrapper around anything else (`f = partial(g, x)`) is not followed + c = self.canon(m.call_alias[parts[i]], seen) + if c in self.classes: + return self.canon('.'.join([c] + parts[i + 1:]), seen) + return None + if i + 1 < len(parts): # Class.method + c = self.canon('.'.join(parts[:i + 1]), seen) + if c in self.classes: + r = self.lookup(c, parts[i + 1]) + return r if len(parts) == i + 2 else None + return None + return None + + def type_of(self, hint): + """the canonical library class a use hint names: ('t', T) the object's own declared type, ('f', B, f) the + declared type of field f on B (annotated in the class, or assigned from an annotated parameter)""" + if not hint: + return '' + if hint[0] == 't': + c = self.canon(hint[1]) + return c if c in self.classes else '' + b = self.canon(hint[1]) + if b not in self.classes: + return '' + for k in self.mro(b): + t = self.classes[k].ftypes.get(hint[2]) + if t: + c = self.canon(t) or self.canon(f'{self.classes[k].mod.name}.{t}') + return c if c in self.classes else '' + return '' + + def mro(self, c, seen=None): + seen = seen if seen is not None else set() + if c in seen: + return [] + seen.add(c) + out = [c] + k = self.classes.get(c) + for b in (k.bases if k else []): + bc = self.canon(b) + if bc in self.classes: + for x in self.mro(bc, seen): + if x not in out: + out.append(x) + return out + + def lookup(self, c, name): + for k in self.mro(c): + m = self.classes[k].methods.get(name) + if m: + return m + return None + + def solve(self): + S = {q: set() for q in self.fns} # (root, path, member, via) + ST = {c: set() for c in self.classes} # class -> (field, init param) + for f in self.fns.values(): + for ev in f.events: + if ev[0] == 'use' and ev[1][0] == 'p' and (ev[1][1] == 'self' or ev[1][1] in f.pos): + S[f.q].add((ev[1][1], ev[1][2], ev[2], f.q, self.type_of(ev[3]))) + if f.cls and f.q.endswith('.__init__'): + for fld, v in f.stores: + if v[0] == 'p' and v[2] == () and v[1] in f.pos: + ST[f.cls].add((fld, v[1])) + + def init_of(c): + return self.lookup(c, '__init__') + + def stores(c): + out = set() + for k in self.mro(c): + out |= ST.get(k, set()) + return out + + def bind(callee, posv, kwv): + """callee parameter -> the caller's values""" + g = self.fns.get(callee) + if not g: + return {} + out = {} + byp = {i: n for n, i in g.pos.items() if i >= 0} + for i, vs in enumerate(posv): + if vs is not None and i in byp: + out.setdefault(byp[i], []).extend(vs) + for k, vs in kwv.items(): + if k in g.pos: + out.setdefault(k, []).extend(vs) + return out + + def lift(v, path, depth=0): + """a value reached in the caller, followed down `path`: param-rooted values only""" + if v[0] == 'p': + p = v[2] + tuple(path) + return [(v[1], p)] if len(p) <= MAXPATH else [] + if v[0] == 'field' and depth < 3: + return [x for w in field_vals(v[1], v[2]) for x in lift(w, path, depth + 1)] + if v[0] == 'new' and path and depth < 3: + return [x for w in field_vals(v, path[0]) for x in lift(w, path[1:], depth + 1)] + return [] + + def field_vals(newv, fld): + c = self.canon(newv[1]) + if c not in self.classes: + return [] + ini = init_of(c) + if not ini: + return [] + posv = [list(x) if x is not None else None for x in newv[2][0]] + kwv = {k: list(v) for k, v in newv[2][1]} + b = bind(ini, posv, kwv) + return [w for f, pn in stores(c) if f == fld for w in b.get(pn, [])] + + changed, rounds = True, 0 + while changed and rounds < 12: + changed, rounds = False, rounds + 1 + for f in self.fns.values(): + acc = S[f.q]; n0 = len(acc) + for ev in f.events: + if ev[0] == 'use' and ev[1][0] == 'field': + for root, path in lift(ev[1], ()): + if root == 'self' or root in f.pos: + acc.add((root, path, ev[2], f.q, '')) + continue + if ev[0] != 'call': + continue + tgt, posv, kwv = ev[1], ev[2], ev[3] + targets = [] # (callee, selfvals) + if tgt[0] == 'ref': + q = self.canon(tgt[1]) + if q in self.fns: + targets.append((q, [])) + elif q in self.classes and init_of(q): + targets.append((init_of(q), [])) + elif tgt[0] == 'method': + rv, name = tgt[1], tgt[2] + if rv == ('p', 'self', ()) and f.cls: + m = self.lookup(f.cls, name) + if m: + targets.append((m, [rv])) + elif rv[0] == 'p' and rv[2] == () and rv[1] in f.ptypes: + c = self.canon(f.ptypes[rv[1]]) + m = self.lookup(c, name) if c in self.classes else None + if m: + targets.append((m, [rv])) + elif rv[0] == 'p' and len(rv[2]) == 1: + # `self.loader.load(...)`: the field's declared type says whose `load` runs + base = f.cls if rv[1] == 'self' else f.ptypes.get(rv[1]) + c = self.type_of(('f', base, rv[2][0])) if base else '' + m = self.lookup(c, name) if c else None + if m: + targets.append((m, [rv])) + elif rv[0] == 'new': + c = self.canon(rv[1]) + if c in self.classes and self.lookup(c, name): + targets.append((self.lookup(c, name), [rv])) + elif tgt[0] == 'super' and tgt[1]: + for k in self.mro(tgt[1])[1:]: + m = self.classes[k].methods.get(tgt[2]) + if m: + targets.append((m, [('p', 'self', ())])); break + for callee, selfv in targets: + b = bind(callee, posv, kwv) + for (r, p, mem, via, ty) in list(S.get(callee, ())): + src = selfv if r == 'self' else b.get(r, []) + for v in src: + for root, path in lift(v, p): + if root == 'self' or root in f.pos: + acc.add((root, path, mem, via, ty)) + if callee.endswith('.__init__'): + g = self.fns[callee] + # super().__init__(x) / Base.__init__(self, x): what the base stores, this class stores + if f.cls and f.q.endswith('.__init__') and (selfv == [('p', 'self', ())] or tgt[0] == 'super'): + for fld, pn in stores(g.cls): + for v in b.get(pn, []): + if v[0] == 'p' and v[2] == () and v[1] in f.pos and (fld, v[1]) not in ST[f.cls]: + ST[f.cls].add((fld, v[1])); changed = True + if len(acc) != n0: + changed = True + self.S, self.ST = S, ST + return rounds + + def returns(self): + """R[f]: what f's return value is, as far as a decorator needs it -- + ('obj', C, field, param) a new C whose `field` holds f's parameter `param` + ('closure', D) the nested function D (a decorator factory's decorator) + ('param', param) f's own parameter, unchanged (an identity / registering decorator)""" + R = {q: set() for q in self.fns} + + def bindv(g, args): + posv = [list(x) if x is not None else None for x in args[0]] + return self._bind(g, posv, {k: list(v) for k, v in args[1]}) + + def own(f, w): + return w[0] == 'p' and w[2] == () and w[1] in f.pos and w[1] != 'self' + + def apply(f, g, args, out): + b = bindv(g, args) + for d in list(R.get(g, ())): + if d[0] == 'closure': + out.add(d) + elif d[0] in ('obj', 'param'): + for w in b.get(d[-1], []): + if own(f, w): + out.add(d[:-1] + (w[1],)) + elif w[0] == 'closure' and d[0] == 'param': + out.add(w) + + def resolve(f, v, out, depth=0): + if depth > 4: + return + if v[0] == 'closure': + out.add(v) + elif own(f, v): + out.add(('param', v[1])) + elif v[0] == 'new': + c = self.canon(v[1]) + if c in self.classes: + ini = self.lookup(c, '__init__') + if ini: + b = bindv(ini, v[2]) + for fld, pn in self._stores(c): + for w in b.get(pn, []): + if own(f, w): + out.add(('obj', c, fld, w[1])) + elif c in self.fns: + apply(f, c, v[2], out) + elif v[0] == 'res': + apply(f, v[1], v[2], out) + elif v[0] == 'res2': + inner = set() + resolve(f, v[1], inner, depth + 1) + for d in inner: + if d[0] == 'closure': + apply(f, d[1], v[2], out) + + for _ in range(8): + changed = False + for f in self.fns.values(): + acc = R[f.q]; n0 = len(acc) + for v in f.rets: + resolve(f, v, acc) + changed |= len(acc) != n0 + if not changed: + break + self.R = R + + def _bind(self, callee, posv, kwv): + g = self.fns.get(callee) + if not g: + return {} + out = {} + byp = {i: n for n, i in g.pos.items() if i >= 0} + for i, vs in enumerate(posv): + if vs is not None and i in byp: + out.setdefault(byp[i], []).extend(vs) + for k, vs in kwv.items(): + if k in g.pos: + out.setdefault(k, []).extend(vs) + return out + + def _stores(self, c): + out = set() + for k in self.mro(c): + out |= self.ST.get(k, set()) + return out + + def decorates(self, g): + """what applying the callable g as a decorator to a function f yields: (form, class, field) -- DIRECT for + `@g`, FACTORY for `@g(...)`; class '' when f itself comes back (a registering decorator)""" + f = self.fns[g] + first = lambda h: next((n for n, i in self.fns[h].pos.items() if i == 0), None) + out = set() + p0 = first(g) + for d in self.R.get(g, ()): + if d[0] == 'obj' and d[3] == p0: + out.add(('DIRECT', d[1], d[2])) + elif d[0] == 'param' and d[1] == p0: + out.add(('DIRECT', '', '')) + elif d[0] == 'closure': + q0 = first(d[1]) + for e in self.R.get(d[1], ()): + if e[0] == 'obj' and e[3] == q0: + out.add(('FACTORY', e[1], e[2])) + elif e[0] == 'param' and e[1] == q0: + out.add(('FACTORY', '', '')) + return out + + def named(self, imports, lib_pkgs): + """the library classes and functions the client can name: what it imports, what a module it imports + (and that module's direct submodules) declares, and the class a decorator it imports wraps a function + in. Summaries are emitted for these only -- the solve reads what the client can reach, not the venv.""" + out = set() + def add(q): + c = self.canon(q) + if c and c.split('.')[0] in lib_pkgs and '' not in c: + out.add(c) + for p in imports: + add(p) + if p in self.mods: + for m in [p] + [x for x in self.mods if x.startswith(p + '.') and '.' not in x[len(p) + 1:]]: + for local in self.mods[m].ns: + add(f'{m}.{local}') + for q in list(out): + if q in self.fns: + for _, c, _ in self.decorates(q): + if c: + out.add(c) + elif q in self.classes: + for n in {n for k in self.mro(q) for n in self.classes[k].methods}: + m = self.lookup(q, n) + if m: + for _, c, _ in self.decorates(m): + if c: + out.add(c) + return out + + def rows(self, keep): + """flattened rows: every method a class HAS (inherited ones included) and every constructor""" + cb, fi, al, de, an = set(), set(), set(), set(), set() + + def emit(callee_name, fq, kind): + g = self.fns[fq] + for (r, p, mem, via, ty) in self.S.get(fq, ()): + if r == 'self': + pos = -1 + else: + pos = g.pos.get(r, -3) + if pos == -3 or (pos == -1 and kind != 'function'): + continue + cb.add((callee_name, r, pos, '.'.join(p), mem, via, ty)) + for q, g in self.fns.items(): + if g.cls is None and not g.local and keep(q): + emit(q, q, 'function') + for form, c, fld in self.decorates(q): + de.add((q, form, c, fld)) + for c in self.classes: + if not keep(c): + continue + names = set() + for k in self.mro(c): + names |= set(self.classes[k].methods) + for n in names: + m = self.lookup(c, n) + if m and self.fns[m].kind != 'property': + emit(f'{c}.{n}', m, 'method' if self.fns[m].kind != 'static' else 'function') + for form, k, fld in self.decorates(m): + de.add((f'{c}.{n}', form, k, fld)) + for a in self.mro(c)[1:]: + an.add((c, a)) + ini = self.lookup(c, '__init__') + if ini: + emit(c, ini, 'ctor') + g = self.fns[ini] + for fld, pn in set().union(*[self.ST.get(k, set()) for k in self.mro(c)]): + p = g.pos.get(pn, -3) + if p != -3: + fi.add((c, fld, pn, p)) + if p == 0: + de.add((c, 'DIRECT', c, fld)) # `@C def f`: C(f) holds f + for mname, m in self.mods.items(): + for local, q in list(m.ns.items()) + [(k, v) for k, v in m.call_alias.items() if k not in m.ns]: + a = f'{mname}.{local}' + if a == q: + continue + c = self.canon(a) + if c and c != a and keep(c): + al.add((a, c)) + return cb, fi, al, de, an + + +def write(path, head, rows): + tmp = path + '.tmp' + with open(tmp, 'w', newline='', encoding='utf-8') as fh: + w = csv.writer(fh, delimiter='\t', lineterminator='\n') + w.writerow(head) + for r in sorted(rows, key=lambda x: tuple(str(c) for c in x)): + w.writerow(r) + os.replace(tmp, path) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--src', required=True); ap.add_argument('--out', required=True) + ap.add_argument('--ir', default=None); ap.add_argument('--stats', action='store_true') + A = ap.parse_args() + os.makedirs(A.out, exist_ok=True) + heads = (('py-lib-callback.csv', ('callee', 'root', 'pos', 'path', 'member', 'via', 'type')), + ('py-lib-field-init.csv', ('class', 'field', 'param', 'pos')), + ('py-lib-alias.csv', ('alias', 'canonical')), + ('py-lib-decorates.csv', ('decorator', 'form', 'class', 'field')), + ('py-lib-ancestor.csv', ('class', 'ancestor'))) + sites, stdlib = find_sites(os.path.abspath(A.src)) + if not sites: + for f, h in heads: + write(os.path.join(A.out, f), h, []) + print('▶ library callbacks: no Python environment found in the repository (.venv, venv, $VIRTUAL_ENV, ' + 'AXIOMCODE_PY_SITE); none summarised') + return + imports = client_imports(A.src, A.ir or A.out) + # CACHED BY WHAT DECIDES THE ANSWER: this file, the client's import paths, and the installed distributions + # (name and version from each *.dist-info) of every environment read. An edit that adds no import -- every + # refresh but a few -- reuses the tables instead of reading the library again. + h = hashlib.sha256() + h.update(open(os.path.abspath(__file__), 'rb').read()) + h.update('\n'.join(sorted(imports)).encode()) + for d in sites + ([stdlib] if stdlib else []): + h.update(d.encode()) + try: + h.update('\n'.join(sorted(x for x in os.listdir(d) if x.endswith(('.dist-info', '.egg-info', '.pth')))).encode()) + except OSError: + pass + cache = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.join(os.path.expanduser('~'), '.cache'), + 'axiomcode', 'libsum', h.hexdigest()[:32]) + if all(os.path.isfile(os.path.join(cache, f)) for f, _ in heads): + import shutil + for f, _ in heads: + shutil.copyfile(os.path.join(cache, f), os.path.join(A.out, f)) + try: + os.utime(cache) + print(open(os.path.join(cache, 'summary.txt'), encoding='utf-8').read().strip() + ' (cached)') + except OSError: + print('▶ library callbacks: reused the cached summaries') + return + roots = {p.split('.')[0] for p in imports} + prog, todo, seen, client_pkgs = Program(), sorted(roots), set(), set() + # the client's own package installed in editable mode is not a library: its top-level names in the source tree + for n in os.listdir(A.src): + if os.path.isfile(os.path.join(A.src, n, '__init__.py')) or n.endswith('.py'): + client_pkgs.add(n[:-3] if n.endswith('.py') else n) + for sub in ('src', 'lib'): + d = os.path.join(A.src, sub) + if os.path.isdir(d): + for n in os.listdir(d): + if os.path.isfile(os.path.join(d, n, '__init__.py')): + client_pkgs.add(n) + nfiles, loaded = 0, {} + while todo: + r = todo.pop() + if r in seen or r in client_pkgs: + continue + seen.add(r) + pkg = locate(r, sites, stdlib) + if not pkg: + continue + for modname, path in pkg.files(): + m = Module(modname, path, path.endswith('__init__.py')) + if not m.load(): + continue + nfiles += 1 + loaded[m.name] = m + for imp in m.imports: + t = imp.split('.')[0] + if t and t not in seen: + todo.append(t) + # `from M import *` re-exports M's public names (its __all__ when it declares one), so a name the client imports + # through the star module (`collections.abc.MutableMapping`) is the declaration in M. Expanded before any body is + # analysed, to a fixpoint for a star of a star. + for _ in range(4): + grew = False + for m in loaded.values(): + for b in m.stars: + bm = loaded.get(b) + if bm is None: + continue + names = bm.all_names if bm.all_names is not None else [n for n in bm.ns if not n.startswith('_')] + for n in names: + if n in bm.ns and n not in m.ns: + m.ns[n] = bm.ns[n]; grew = True + if not grew: + break + for m in loaded.values(): + m.analyse() + del m.tree + prog.add(m) + rounds = prog.solve() + prog.returns() + lib_pkgs = {p for p in seen if p not in client_pkgs} + named = prog.named(imports, lib_pkgs) + keep = lambda q: q in named + cb, fi, al, de, an = prog.rows(keep) + for (f, h), rows in zip(heads, (cb, fi, al, de, an)): + write(os.path.join(A.out, f), h, rows) + line = (f'▶ library callbacks: {len(cb)} hand-backs, {len(fi)} constructor stores, {len(al)} re-exports, ' + f'{len(de)} decorators, from {nfiles} library modules ({len(lib_pkgs)} packages, {rounds} rounds)') + print(line) + try: + import shutil + tmp = cache + f'.tmp{os.getpid()}' + os.makedirs(tmp, exist_ok=True) + for f, _ in heads: + shutil.copyfile(os.path.join(A.out, f), os.path.join(tmp, f)) + open(os.path.join(tmp, 'summary.txt'), 'w', encoding='utf-8').write(line + '\n') + os.replace(tmp, cache) + root = os.path.dirname(cache) # keep the 64 most recently used + old = sorted((os.path.getmtime(os.path.join(root, x)), x) for x in os.listdir(root) if '.tmp' not in x) + for _, x in old[:-64]: + shutil.rmtree(os.path.join(root, x), ignore_errors=True) + except OSError: + pass + + +if __name__ == '__main__': + main() diff --git a/graph/python/souffle/decls_all.dl b/graph/python/souffle/decls_all.dl index 9aa06420..08cc0035 100644 --- a/graph/python/souffle/decls_all.dl +++ b/graph/python/souffle/decls_all.dl @@ -52,6 +52,11 @@ .decl py_fixture_value_method(v:symbol) .decl py_parametrize_deco(v:symbol) .decl py_parametrize_argnames_kw(v:symbol) +.decl py_parametrize_argvalues_kw(v:symbol) +.decl py_fixture_params_kw(v:symbol) +.decl py_param_row_wrapper(v:symbol) +.decl py_fixture_request_param(v:symbol) +.decl py_request_param_attr(v:symbol) .decl py_parametrize_indirect_kw(v:symbol) .decl py_parametrize_indirect_all(v:symbol) .decl py_argnames_sep(v:symbol) @@ -238,6 +243,7 @@ .decl element_lib_type_of(c0:symbol,c1:symbol) .decl binding_element_lib_type(c0:symbol,c1:symbol) .decl param_declared_type_by_parser(c0:symbol,c1:symbol,c2:symbol) +.decl union_operand(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_element(c0:symbol,c1:symbol,c2:symbol) .decl annotation_owner_module(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl annotation_container_kind(c0:symbol) @@ -260,6 +266,7 @@ .decl mro_tie(c0:symbol,c1:symbol,c2:symbol) .decl mro_lookup(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_data_shadow(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl replaced_method_of(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_decorated(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_opaque(c0:symbol,c1:symbol,c2:symbol) .decl mro_definer_after(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) @@ -312,6 +319,9 @@ .decl py_path_join_function(c0:symbol) .decl py_path_join_operator(c0:symbol) .decl py_copy_function(c0:symbol) +.decl py_returns_arg(c0:symbol,c1:symbol) +.decl py_passthrough_arg(c0:symbol,c1:symbol) +.decl py_passthrough_root(c0:symbol,c1:symbol) .decl dict_lookup_method(c0:symbol) .decl builtin_target(c0:symbol,c1:symbol) .decl builtin_object_init_target(c0:symbol) @@ -402,6 +412,7 @@ .decl method_owner_declares_param(c0:symbol,c1:symbol,c2:symbol) .decl call_returns_type(c0:symbol,c1:symbol) .decl call_returns_element(c0:symbol,c1:symbol) +.decl root_expr_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl iter_pair(c0:symbol,c1:symbol,c2:symbol) .decl with_pair(c0:symbol,c1:symbol,c2:symbol) .decl with_target_declared(c0:symbol,c1:symbol,c2:symbol) @@ -524,10 +535,14 @@ .decl field_holds_method(c0:symbol,c1:symbol,c2:symbol) .decl type_attr_method_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_attr_class_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl dict_view_name(c0:symbol) +.decl dict_items_view(c0:symbol) .decl computed_name_prefix(c0:symbol,c1:symbol) .decl expr_operator(c0:symbol,c1:symbol,c2:symbol) .decl local_holds_untyped_return(c0:symbol,c1:symbol) .decl getattr_computed_name(c0:symbol,c1:symbol,c2:symbol) +.decl getattr_computed_type(c0:symbol,c1:symbol,c2:symbol) +.decl getattr_computed_member(c0:symbol,c1:symbol) .decl method_is_async(c0:symbol,c1:symbol) .decl await_operand(c0:symbol,c1:symbol,c2:symbol) .decl subscript_index_is_slice(c0:symbol,c1:symbol) @@ -592,6 +607,8 @@ .decl decorator_hits_lib(c0:symbol,c1:symbol) .decl decorator_hits_builtin(c0:symbol,c1:symbol) .decl property_read_edge(c0:symbol,c1:symbol,c2:symbol) +.decl property_write_edge(c0:symbol,c1:symbol,c2:symbol) +.decl type_property_accessor(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl with_protocol_edge(c0:symbol,c1:symbol,c2:symbol) .decl iteration_protocol_sync(c0:symbol) .decl iteration_protocol_of(c0:symbol,c1:symbol) @@ -617,6 +634,8 @@ .decl protocol_edge_wide(c0:symbol,c1:symbol,c2:number) .decl site_has_boundary(c0:symbol) .decl site_class(c0:symbol,c1:symbol) +.decl attr_access(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl site_class_kind(c0:symbol) .decl site_reason(c0:symbol,c1:symbol) .decl site_is_decorator(c0:symbol) .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) @@ -658,3 +677,41 @@ .decl binding_declared_call_protocol(c0:symbol,c1:symbol,c2:symbol) .decl binding_assigned_lambda(c0:symbol,c1:symbol,c2:symbol) .decl instance_attr_method_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl dict_view_slot(c0:symbol,c1:symbol) +.decl mapping_annotation_of(c0:symbol,c1:symbol) +.decl type_ref_slot(c0:symbol,c1:symbol,c2:symbol) +.decl dict_view_call(c0:symbol,c1:symbol,c2:symbol) +.decl dict_item_slot_type(c0:symbol,c1:symbol,c2:symbol) +.decl method_returns_computed_name(c0:symbol) +.decl computed_name_value(c0:symbol) +.decl call_computed_name_family(c0:symbol) +.decl py_fixture_value_class(c0:symbol,c1:symbol) +.decl builtin_protocol_slot(c0:symbol,c1:symbol) +.decl builtin_protocol_fallback(c0:symbol,c1:symbol,c2:symbol) +.decl builtin_protocol_arg(c0:symbol,c1:symbol,c2:symbol) +.decl builtin_protocol_edge(c0:symbol,c1:symbol,c2:symbol) +.decl operator_protocol_slot(c0:symbol,c1:symbol,c2:symbol) +.decl operator_protocol_fallback(c0:symbol,c1:symbol) +.decl membership_operator(c0:symbol) +.decl membership_protocol_slot(c0:symbol) +.decl membership_protocol_fallback(c0:symbol,c1:symbol) +.decl unary_protocol_slot(c0:symbol,c1:symbol) +.decl truth_negation_operator(c0:symbol) +.decl truth_protocol_fallback(c0:symbol,c1:symbol) +.decl truth_test_root(c0:symbol) +.decl truth_test_role(c0:symbol) +.decl operator_protocol_edge(c0:symbol,c1:symbol,c2:symbol) +.decl operator_type_has(c0:symbol,c1:symbol) +.decl membership_type_has(c0:symbol,c1:symbol) +.decl truth_tested(c0:symbol) +.decl truth_protocol_edge(c0:symbol,c1:symbol,c2:symbol) +.decl truth_type_has(c0:symbol,c1:symbol) +.decl type_has_member(c0:symbol,c1:symbol) +// staged beside the client IR by graph/python/libsum/libsum.py (framework-behavior/library-callbacks.dl) +.decl py_lib_callback(callee:symbol, root:symbol, pos:symbol, path:symbol, member:symbol, via:symbol, type:symbol) +.decl py_lib_ancestor(cls:symbol, ancestor:symbol) +.decl py_lib_field_init(cls:symbol, field:symbol, param:symbol, pos:symbol) +.decl py_lib_alias(alias:symbol, canon:symbol) +.decl lib_callback_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) +.decl lib_callback_site(c0:symbol,c1:symbol) +.decl py_lib_decorates(decorator:symbol, form:symbol, cls:symbol, field:symbol) diff --git a/graph/python/souffle/export_manifest.tsv b/graph/python/souffle/export_manifest.tsv index 758e6d9e..19aaa3d2 100644 --- a/graph/python/souffle/export_manifest.tsv +++ b/graph/python/souffle/export_manifest.tsv @@ -91,3 +91,5 @@ remote_unserved remote-unserved.csv remote_unsent remote-unsent.csv framework_edge framework-edge.csv framework_unjoined framework-unjoined.csv +lib_callback_edge lib-callback-edge.csv +lib_callback_site lib-callback-site.csv diff --git a/graph/python/templates/client-extra.map b/graph/python/templates/client-extra.map new file mode 100644 index 00000000..18f5076f --- /dev/null +++ b/graph/python/templates/client-extra.map @@ -0,0 +1,12 @@ +# Soufflé input-relation -> CSV basename for facts staged BESIDE the client IR that are not parser facts. +# Same format and staging as client-ir.map (absent file = empty relation, so every project compiles one program), +# but outside the frozen parser schema: no decls_base.dl row, no lib.map counterpart (a library's own summary of +# its dependencies is not staged), declared in decls_all.dl instead. +# +# The library callback summaries graph/python/libsum/libsum.py writes from the project's installed dependencies +# (framework-behavior/library-callbacks.dl). Absent = empty = no library hand-backs, exactly as before. +py_lib_callback py-lib-callback +py_lib_field_init py-lib-field-init +py_lib_alias py-lib-alias +py_lib_decorates py-lib-decorates +py_lib_ancestor py-lib-ancestor diff --git a/graph/test/csharp/README.md b/graph/test/csharp/README.md index 530ecc59..1663ccb8 100644 --- a/graph/test/csharp/README.md +++ b/graph/test/csharp/README.md @@ -306,7 +306,7 @@ dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle dotnet build -c Release graph/test/csharp/runtime-oracle/AxiomCsInstrument ``` -`node` must be 18 or newer, and the SAME version across a baseline and a fix run: a +`node` must be 22.13 or newer (what the repository itself requires), and the SAME version across a baseline and a fix run: a number measured on one version and compared against another is not a measurement. Every script exits 77 when a tool it needs is missing, so a machine without the toolchain skips rather than reporting a false failure. diff --git a/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js b/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js new file mode 100644 index 00000000..e6d3397f --- /dev/null +++ b/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js @@ -0,0 +1,27 @@ +'use strict'; +// A member call whose receiver is a value of a package no IR declares: what calling it, +// constructing from it, or chaining on it returns is the package's object, so the +// project's own `get` is not what these call (library_receiver in the diagnostics). +const request = require('never-staged'); +const wire = require('alpha/missing'); +class Repo { get(id) { return id; } } +class Service { + constructor(repo) { this.repo = repo; } + load(id) { return this.repo.get(id); } +} +async function probe(app) { + await request(app).get('/x').expect(200); + const res = await request(app).get('/y'); + const md = new wire.Metadata(); + md.get('k'); + return res.body; +} +// controls: none of these is a package's value +function untyped(obj) { return obj.get(1); } +function wrapped(repo) { return request.mocked(new Repo()).get(1); } +function reassigned() { + let c = request.agent(); + c = untyped; + return c.get(2); +} +module.exports = { Repo, Service, probe, untyped, wrapped, reassigned }; diff --git a/graph/test/javascript/expected/08-library-dependency.diag b/graph/test/javascript/expected/08-library-dependency.diag index aaefacd3..38b34560 100644 --- a/graph/test/javascript/expected/08-library-dependency.diag +++ b/graph/test/javascript/expected/08-library-dependency.diag @@ -1,6 +1,9 @@ import_cause app.js:2:13 dep not_staged import_cause app.js:3:17 dep not_staged import_cause app.js:3:9 dep not_staged +library_receiver app.js:5:1 c.get dep +library_receiver app.js:6:1 new Client().request dep +library_receiver app.js:8:1 dep.helper dep package_entry probe6-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved app.js:4:11 FUNCTION_CALL dep callee_untyped unresolved app.js:5:1 METHOD_CALL get receiver_untyped diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.diag b/graph/test/javascript/expected/28-uninstalled-dependency.diag index 2a015d3b..b3376dc9 100644 --- a/graph/test/javascript/expected/28-uninstalled-dependency.diag +++ b/graph/test/javascript/expected/28-uninstalled-dependency.diag @@ -4,6 +4,21 @@ import_cause app.js:6:14 alpha/missing not_staged import_cause app.js:7:14 never-staged not_staged import_cause app.mjs:1:10 alpha not_staged import_cause app.mjs:2:17 alpha not_staged +import_cause receivers.js:5:17 never-staged not_staged +import_cause receivers.js:6:14 alpha/missing not_staged +library_receiver app.js:10:3 alpha.createClient alpha +library_receiver app.js:10:3 alpha.createClient().run alpha +library_receiver app.js:13:3 gone.anything alpha/missing +library_receiver app.js:14:3 nope.thing never-staged +library_receiver app.mjs:3:1 createClient().run alpha +library_receiver app.mjs:4:1 m.createClient alpha +library_receiver app.mjs:4:1 m.createClient().run alpha +library_receiver receivers.js:13:9 request(app).get never-staged +library_receiver receivers.js:13:9 request(app).get('/x').expect never-staged +library_receiver receivers.js:14:21 request(app).get never-staged +library_receiver receivers.js:16:3 md.get alpha/missing +library_receiver receivers.js:21:33 request.mocked never-staged +library_receiver receivers.js:23:11 request.agent never-staged package_entry case26-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved app.js:10:3 METHOD_CALL createClient receiver_untyped unresolved app.js:10:3 METHOD_CALL run receiver_untyped @@ -16,3 +31,16 @@ unresolved app.mjs:3:1 FUNCTION_CALL createClient callee_untyped unresolved app.mjs:3:1 METHOD_CALL run receiver_untyped unresolved app.mjs:4:1 METHOD_CALL createClient receiver_untyped unresolved app.mjs:4:1 METHOD_CALL run receiver_untyped +unresolved receivers.js:10:21 METHOD_CALL get receiver_untyped +unresolved receivers.js:13:9 FUNCTION_CALL request callee_untyped +unresolved receivers.js:13:9 METHOD_CALL expect receiver_untyped +unresolved receivers.js:13:9 METHOD_CALL get receiver_untyped +unresolved receivers.js:14:21 FUNCTION_CALL request callee_untyped +unresolved receivers.js:14:21 METHOD_CALL get receiver_untyped +unresolved receivers.js:15:14 CONSTRUCTOR_CALL Metadata receiver_untyped +unresolved receivers.js:16:3 METHOD_CALL get receiver_untyped +unresolved receivers.js:20:32 METHOD_CALL get receiver_untyped +unresolved receivers.js:21:33 METHOD_CALL get receiver_untyped +unresolved receivers.js:21:33 METHOD_CALL mocked receiver_untyped +unresolved receivers.js:23:11 METHOD_CALL agent receiver_untyped +unresolved receivers.js:25:10 METHOD_CALL get member_absent diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.edges b/graph/test/javascript/expected/28-uninstalled-dependency.edges index cb072a1a..11563c90 100644 --- a/graph/test/javascript/expected/28-uninstalled-dependency.edges +++ b/graph/test/javascript/expected/28-uninstalled-dependency.edges @@ -11,3 +11,17 @@ app.mjs:3:1 FUNCTION_CALL createClient -> ambiguous_unknown - app.mjs:3:1 METHOD_CALL createClient().run -> ambiguous_unknown - app.mjs:4:1 METHOD_CALL m.createClient -> ambiguous_unknown - app.mjs:4:1 METHOD_CALL m.createClient().run -> ambiguous_unknown - +receivers.js:10:21 METHOD_CALL this.repo.get -> ambiguous_unknown - +receivers.js:13:9 FUNCTION_CALL request -> ambiguous_unknown - +receivers.js:13:9 METHOD_CALL request(app).get -> ambiguous_unknown - +receivers.js:13:9 METHOD_CALL request(app).get('/x').expect -> ambiguous_unknown - +receivers.js:14:21 FUNCTION_CALL request -> ambiguous_unknown - +receivers.js:14:21 METHOD_CALL request(app).get -> ambiguous_unknown - +receivers.js:15:14 CONSTRUCTOR_CALL wire.Metadata -> ambiguous_unknown - +receivers.js:16:3 METHOD_CALL md.get -> ambiguous_unknown - +receivers.js:20:32 METHOD_CALL obj.get -> ambiguous_unknown - +receivers.js:21:33 METHOD_CALL request.mocked -> ambiguous_unknown - +receivers.js:21:33 METHOD_CALL request.mocked(new Repo()).get -> ambiguous_unknown - +receivers.js:21:48 CONSTRUCTOR_CALL Repo -> implicit_constructor - +receivers.js:23:11 METHOD_CALL request.agent -> ambiguous_unknown - +receivers.js:25:10 METHOD_CALL c.get -> ambiguous_unknown - diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag b/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag index 1e4b56db..cb5c2a22 100644 --- a/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag +++ b/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag @@ -1,5 +1,15 @@ import_cause app.js:6:14 alpha/missing staged_unreached import_cause app.js:7:14 never-staged not_staged +import_cause receivers.js:5:17 never-staged not_staged +import_cause receivers.js:6:14 alpha/missing staged_unreached +library_receiver app.js:13:3 gone.anything alpha/missing +library_receiver app.js:14:3 nope.thing never-staged +library_receiver receivers.js:13:9 request(app).get never-staged +library_receiver receivers.js:13:9 request(app).get('/x').expect never-staged +library_receiver receivers.js:14:21 request(app).get never-staged +library_receiver receivers.js:16:3 md.get alpha/missing +library_receiver receivers.js:21:33 request.mocked never-staged +library_receiver receivers.js:23:11 request.agent never-staged package_entry alpha . [] MAIN lib/index.js RESOLVED -> lib:lib/index.js package_entry alpha . [import] EXPORTS lib/index.mjs RESOLVED -> lib:lib/index.mjs package_entry alpha . [require] EXPORTS lib/index.js RESOLVED -> lib:lib/index.js @@ -9,3 +19,16 @@ package_entry case26-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved app.js:13:3 METHOD_CALL anything receiver_untyped unresolved app.js:14:3 METHOD_CALL thing receiver_untyped unresolved app.mjs:2:17 DYNAMIC_IMPORT_CALL dynamic +unresolved receivers.js:10:21 METHOD_CALL get receiver_untyped +unresolved receivers.js:13:9 FUNCTION_CALL request callee_untyped +unresolved receivers.js:13:9 METHOD_CALL expect receiver_untyped +unresolved receivers.js:13:9 METHOD_CALL get receiver_untyped +unresolved receivers.js:14:21 FUNCTION_CALL request callee_untyped +unresolved receivers.js:14:21 METHOD_CALL get receiver_untyped +unresolved receivers.js:15:14 CONSTRUCTOR_CALL Metadata receiver_untyped +unresolved receivers.js:16:3 METHOD_CALL get receiver_untyped +unresolved receivers.js:20:32 METHOD_CALL get receiver_untyped +unresolved receivers.js:21:33 METHOD_CALL get receiver_untyped +unresolved receivers.js:21:33 METHOD_CALL mocked receiver_untyped +unresolved receivers.js:23:11 METHOD_CALL agent receiver_untyped +unresolved receivers.js:25:10 METHOD_CALL get member_absent diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges b/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges index 6dfe1d1b..952c1e2c 100644 --- a/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges +++ b/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges @@ -11,3 +11,17 @@ app.mjs:3:1 FUNCTION_CALL createClient -> boundary_lib lib:lib/index.mjs:2:1 app.mjs:3:1 METHOD_CALL createClient().run -> boundary_lib lib:lib/index.mjs:1:21 run app.mjs:4:1 METHOD_CALL m.createClient -> boundary_lib lib:lib/index.mjs:2:1 createClient app.mjs:4:1 METHOD_CALL m.createClient().run -> boundary_lib lib:lib/index.mjs:1:21 run +receivers.js:10:21 METHOD_CALL this.repo.get -> ambiguous_unknown - +receivers.js:13:9 FUNCTION_CALL request -> ambiguous_unknown - +receivers.js:13:9 METHOD_CALL request(app).get -> ambiguous_unknown - +receivers.js:13:9 METHOD_CALL request(app).get('/x').expect -> ambiguous_unknown - +receivers.js:14:21 FUNCTION_CALL request -> ambiguous_unknown - +receivers.js:14:21 METHOD_CALL request(app).get -> ambiguous_unknown - +receivers.js:15:14 CONSTRUCTOR_CALL wire.Metadata -> ambiguous_unknown - +receivers.js:16:3 METHOD_CALL md.get -> ambiguous_unknown - +receivers.js:20:32 METHOD_CALL obj.get -> ambiguous_unknown - +receivers.js:21:33 METHOD_CALL request.mocked -> ambiguous_unknown - +receivers.js:21:33 METHOD_CALL request.mocked(new Repo()).get -> ambiguous_unknown - +receivers.js:21:48 CONSTRUCTOR_CALL Repo -> implicit_constructor - +receivers.js:23:11 METHOD_CALL request.agent -> ambiguous_unknown - +receivers.js:25:10 METHOD_CALL c.get -> ambiguous_unknown - diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.oracle b/graph/test/javascript/expected/28-uninstalled-dependency.oracle index 6d77f120..d56dda00 100644 --- a/graph/test/javascript/expected/28-uninstalled-dependency.oracle +++ b/graph/test/javascript/expected/28-uninstalled-dependency.oracle @@ -1,3 +1,4 @@ app.js:11:3 CONSTRUCTOR_CALL Mine SYNTHESIZED_OK app.js:16:1 FUNCTION_CALL main EXACT app.js:9:1 +receivers.js:21:48 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK # defects: 0 diff --git a/graph/test/javascript/expected/30-subpath-pattern-export.diag b/graph/test/javascript/expected/30-subpath-pattern-export.diag index ce53dcce..9a41da01 100644 --- a/graph/test/javascript/expected/30-subpath-pattern-export.diag +++ b/graph/test/javascript/expected/30-subpath-pattern-export.diag @@ -6,6 +6,9 @@ import_cause app.js:7:18 kit/internal/secret not_staged import_cause app.js:8:16 kit/feat/deep/none not_staged import_cause app.mjs:1:10 kit/mjs/gamma not_staged import_cause app.mjs:2:10 kit/feat/alpha not_staged +library_receiver app.js:9:19 createApp().start kit +library_receiver app.js:9:66 internal.secret kit/internal/secret +library_receiver app.js:9:85 nested.x kit/feat/deep/none package_entry case30-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved app.js:9:19 FUNCTION_CALL createApp callee_untyped unresolved app.js:9:19 METHOD_CALL start receiver_untyped diff --git a/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag b/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag index 3f4e3746..4844004a 100644 --- a/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag +++ b/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag @@ -1,5 +1,7 @@ import_cause app.js:7:18 kit/internal/secret staged_unreached import_cause app.js:8:16 kit/feat/deep/none staged_unreached +library_receiver app.js:9:66 internal.secret kit/internal/secret +library_receiver app.js:9:85 nested.x kit/feat/deep/none package_entry case30-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - package_entry kit . [default] EXPORTS src/index.js RESOLVED -> lib:src/index.js package_entry kit . [node.require] EXPORTS src/index.js RESOLVED -> lib:src/index.js diff --git a/graph/test/javascript/expected/34-options-object-callbacks.diag b/graph/test/javascript/expected/34-options-object-callbacks.diag index 2260affa..1d54eea4 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.diag @@ -3,6 +3,8 @@ import_cause main.js:2:14 walker not_staged import_cause main.js:37:136 events builtin import_cause main.js:3:15 https builtin import_cause main.js:4:9 stream builtin +library_receiver main.js:43:25 walk.register walker +library_receiver main.js:43:48 walk.register walker package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved main.js:10:28 FUNCTION_CALL walk callee_untyped unresolved main.js:11:89 FUNCTION_CALL walk callee_untyped diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag index ed5868cc..e30f6720 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag @@ -2,6 +2,8 @@ import_cause main.js:17:16 assert builtin import_cause main.js:37:136 events builtin import_cause main.js:3:15 https builtin import_cause main.js:4:9 stream builtin +library_receiver main.js:43:25 walk.register walker +library_receiver main.js:43:48 walk.register walker package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> - package_entry walker . [] MAIN index.js RESOLVED -> lib:index.js unresolved main.js:12:86 METHOD_CALL destroy no_target diff --git a/graph/test/javascript/expected/48-umd-library.diag b/graph/test/javascript/expected/48-umd-library.diag index 7f81443e..dd637fad 100644 --- a/graph/test/javascript/expected/48-umd-library.diag +++ b/graph/test/javascript/expected/48-umd-library.diag @@ -1,5 +1,7 @@ import_cause app.js:3:13 umdlib not_staged import_cause app.js:4:9 umdlib not_staged +library_receiver app.js:5:1 umd.format umdlib +library_receiver app.js:7:1 new umd.Formatter().render umdlib package_entry case48-app . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved app.js:5:1 METHOD_CALL format receiver_untyped unresolved app.js:6:1 FUNCTION_CALL format callee_untyped diff --git a/graph/test/javascript/expected/60-ts-emit-helpers.diag b/graph/test/javascript/expected/60-ts-emit-helpers.diag index 9cb642a2..f4182214 100644 --- a/graph/test/javascript/expected/60-ts-emit-helpers.diag +++ b/graph/test/javascript/expected/60-ts-emit-helpers.diag @@ -1,4 +1,5 @@ import_cause main.js:5:9 tspkg not_staged +library_receiver main.js:7:26 new Client().send tspkg package_entry 60-ts-emit-helpers . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved main.js:7:26 CONSTRUCTOR_CALL Client callee_untyped unresolved main.js:7:26 METHOD_CALL send receiver_untyped diff --git a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag index ce31a3fe..cc4ccc71 100644 --- a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag +++ b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag @@ -6,6 +6,7 @@ import_cause app.jsx:3:10 thing-kit not_staged import_cause defmemo.jsx:1:10 react not_staged import_cause parts.jsx:1:10 react not_staged import_cause parts.jsx:1:16 react not_staged +library_receiver app.jsx:11:16 React.memo react package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - unresolved app.jsx:10:18 FUNCTION_CALL observer callee_untyped unresolved app.jsx:11:16 METHOD_CALL memo receiver_untyped diff --git a/graph/test/javascript/expected/64-memoize-wrapper-result.edges b/graph/test/javascript/expected/64-memoize-wrapper-result.edges index c3d46e25..46a76a11 100644 --- a/graph/test/javascript/expected/64-memoize-wrapper-result.edges +++ b/graph/test/javascript/expected/64-memoize-wrapper-result.edges @@ -1,6 +1,8 @@ app.js:11:3 FUNCTION_CALL getAlpha -> known_edge memoize.js:5:10 +app.js:11:3 FUNCTION_CALL getAlpha -> multi_inferred app.js:7:26 app.js:11:3 METHOD_CALL getAlpha().getHooks -> known_edge alpha.js:2:3 getHooks app.js:12:3 FUNCTION_CALL getBeta -> known_edge memoize.js:5:10 +app.js:12:3 FUNCTION_CALL getBeta -> multi_inferred app.js:8:25 app.js:12:3 METHOD_CALL getBeta().getHooks -> known_edge beta.js:2:3 getHooks app.js:16:20 FUNCTION_CALL once -> callback_registered app.js:16:25 app.js:16:20 FUNCTION_CALL once -> known_edge once.js:2:1 once @@ -15,12 +17,16 @@ app.js:19:19 FUNCTION_CALL lazy -> callback_registered app.js:19:24 app.js:19:19 FUNCTION_CALL lazy -> known_edge once.js:14:1 lazy app.js:19:30 CONSTRUCTOR_CALL AlphaPlugin -> implicit_constructor - app.js:22:3 FUNCTION_CALL firstAlpha -> known_edge once.js:5:10 +app.js:22:3 FUNCTION_CALL firstAlpha -> multi_inferred app.js:16:25 app.js:22:3 METHOD_CALL firstAlpha().run -> known_edge alpha.js:3:3 run app.js:23:3 FUNCTION_CALL makeBeta -> known_edge once.js:15:10 +app.js:23:3 FUNCTION_CALL makeBeta -> multi_inferred app.js:17:23 app.js:23:3 METHOD_CALL makeBeta().run -> known_edge beta.js:3:3 run app.js:24:3 FUNCTION_CALL firstBeta -> known_edge once.js:5:10 +app.js:24:3 FUNCTION_CALL firstBeta -> multi_inferred app.js:18:24 app.js:24:3 METHOD_CALL firstBeta().run -> known_edge beta.js:3:3 run app.js:25:3 FUNCTION_CALL makeAlpha -> known_edge once.js:15:10 +app.js:25:3 FUNCTION_CALL makeAlpha -> multi_inferred app.js:19:24 app.js:25:3 METHOD_CALL makeAlpha().run -> known_edge alpha.js:3:3 run app.js:29:18 FUNCTION_CALL constant -> known_edge once.js:18:1 constant app.js:29:27 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - @@ -57,10 +63,14 @@ declared.js:27:16 FUNCTION_CALL pickShared -> callback_registered declared.js declared.js:27:16 FUNCTION_CALL pickShared -> known_edge declared.js:20:1 pickShared declared.js:27:33 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - declared.js:30:3 FUNCTION_CALL declAlpha -> known_edge declared.js:9:3 inner +declared.js:30:3 FUNCTION_CALL declAlpha -> multi_inferred declared.js:22:28 declared.js:30:3 METHOD_CALL declAlpha().run -> known_edge alpha.js:3:3 run declared.js:31:3 FUNCTION_CALL declBeta -> known_edge declared.js:9:3 inner +declared.js:31:3 FUNCTION_CALL declBeta -> multi_inferred declared.js:23:27 declared.js:31:3 METHOD_CALL declBeta().run -> known_edge beta.js:3:3 run declared.js:32:3 FUNCTION_CALL declOfGetter -> known_edge declared.js:9:3 inner +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred declared.js:24:26 +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred memoize.js:5:10 declared.js:32:3 METHOD_CALL declOfGetter().run -> known_edge alpha.js:3:3 run declared.js:33:3 FUNCTION_CALL tappedDecl -> known_edge declared.js:15:3 inner declared.js:33:3 METHOD_CALL tappedDecl().markOnly -> known_edge declared.js:13:14 markOnly @@ -202,24 +212,33 @@ shapes.js:45:27 FUNCTION_CALL memoize -> callback_registered shapes.js:45:35 shapes.js:45:27 FUNCTION_CALL memoize -> known_edge memoize.js:2:17 shapes.js:45:41 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - shapes.js:48:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:48:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:48:3 METHOD_CALL getFb().run -> known_edge alpha.js:3:3 run shapes.js:49:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:49:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:49:3 METHOD_CALL getFb().fallbackOnly -> known_edge shapes.js:8:18 fallbackOnly shapes.js:50:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:50:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:50:3 METHOD_CALL getOr().run -> ambient_terminal - shapes.js:50:3 METHOD_CALL getOr().run -> multi_inferred beta.js:3:3 run shapes.js:51:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:51:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:51:3 METHOD_CALL getOr().mockOnly -> ambient_terminal - shapes.js:51:3 METHOD_CALL getOr().mockOnly -> multi_inferred shapes.js:9:14 mockOnly shapes.js:52:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:52:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:52:3 METHOD_CALL getD().run -> known_edge alpha.js:3:3 run shapes.js:53:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:53:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:53:3 METHOD_CALL getD().defaultsOnly -> known_edge shapes.js:25:20 defaultsOnly shapes.js:54:3 FUNCTION_CALL getDefault -> known_edge shapes.js:33:44 shapes.js:54:3 METHOD_CALL getDefault().defaultOnly -> known_edge shapes.js:10:19 defaultOnly shapes.js:55:3 FUNCTION_CALL getSpread -> known_edge shapes.js:33:44 shapes.js:55:3 METHOD_CALL getSpread().run -> ambiguous_unknown - shapes.js:56:3 FUNCTION_CALL warm -> known_edge shapes.js:34:32 +shapes.js:56:3 FUNCTION_CALL warm -> multi_inferred shapes.js:43:20 shapes.js:56:3 METHOD_CALL warm().run -> known_edge alpha.js:3:3 run shapes.js:57:3 FUNCTION_CALL getNested -> known_edge memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred shapes.js:45:35 shapes.js:57:3 METHOD_CALL getNested().run -> known_edge beta.js:3:3 run diff --git a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges index d56c695d..8373c4e3 100644 --- a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges +++ b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges @@ -2,51 +2,66 @@ app.js:10:23 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:10:3 FUNCTION_CALL lazyOrDefault -> callback_registered app.js:10:17 app.js:10:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> known_edge wrap.js:19:10 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred app.js:10:17 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred wrap.js:18:15 app.js:10:3 METHOD_CALL lazyOrDefault(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:11:3 FUNCTION_CALL lazyNullish -> known_edge wrap.js:22:1 lazyNullish app.js:11:3 FUNCTION_CALL lazyNullish() -> known_edge wrap.js:24:10 +app.js:11:3 FUNCTION_CALL lazyNullish() -> multi_inferred wrap.js:23:10 app.js:11:3 METHOD_CALL lazyNullish()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly app.js:15:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:15:3 FUNCTION_CALL plain -> callback_registered app.js:15:9 app.js:15:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:15:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:15:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:15:9 app.js:15:3 METHOD_CALL plain(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:16:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:16:3 FUNCTION_CALL plain -> callback_registered app.js:16:9 app.js:16:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:16:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:16:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:16:9 app.js:16:3 METHOD_CALL plain(() => new A())().replacedOnly -> ambiguous_unknown - app.js:17:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:17:3 FUNCTION_CALL otherWritten -> callback_registered app.js:17:16 app.js:17:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:17:16 app.js:17:3 METHOD_CALL otherWritten(() => new A(), null)().aOnly -> known_edge wrap.js:2:11 aOnly app.js:18:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:18:3 FUNCTION_CALL otherWritten -> callback_registered app.js:18:16 app.js:18:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:18:16 app.js:18:3 METHOD_CALL otherWritten(() => new A(), null)().replacedOnly -> ambiguous_unknown - app.js:4:14 FUNCTION_CALL swapped -> callback_registered app.js:4:22 app.js:4:14 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped app.js:4:28 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:5:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:5:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:5:3 METHOD_CALL sw().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:6:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:6:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:6:3 METHOD_CALL sw().aOnly -> ambiguous_unknown - app.js:7:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> known_edge wrap.js:14:10 +app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> multi_inferred wrap.js:13:38 app.js:7:3 METHOD_CALL lazyGuard(undefined)().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:8:19 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:8:3 FUNCTION_CALL lazyGuard -> callback_registered app.js:8:13 app.js:8:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> known_edge wrap.js:14:10 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred app.js:8:13 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred wrap.js:13:38 app.js:8:3 METHOD_CALL lazyGuard(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:9:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:9:3 FUNCTION_CALL lazyOrDefault() -> known_edge wrap.js:19:10 +app.js:9:3 FUNCTION_CALL lazyOrDefault() -> multi_inferred wrap.js:18:15 app.js:9:3 METHOD_CALL lazyOrDefault()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly many.js:11:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:11:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:11:3 METHOD_CALL s21().replacedOnly -> known_edge wrap.js:3:18 replacedOnly many.js:12:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:12:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:12:3 METHOD_CALL s21().aOnly -> ambiguous_unknown - many.js:3:13 FUNCTION_CALL swapped -> callback_registered many.js:3:21 many.js:3:13 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.diag b/graph/test/javascript/expected/70-single-file-component-scripts.diag index f01b3a10..55bf1e5c 100644 --- a/graph/test/javascript/expected/70-single-file-component-scripts.diag +++ b/graph/test/javascript/expected/70-single-file-component-scripts.diag @@ -1,6 +1,7 @@ import_cause components/List.svelte:2:12 svelte not_staged import_cause components/Price.vue:4:10 vue not_staged import_cause main.js:2:10 vue not_staged +library_receiver main.js:5:1 createApp(Price).mount vue package_entry single-file-component-scripts . [] DEFAULT_INDEX index.js MISSING_FILE -> - unresolved components/Legacy.vue:7:26 METHOD_CALL $emit member_absent unresolved components/List.svelte:5:3 FUNCTION_CALL onMount callee_untyped diff --git a/graph/test/javascript/expected/74-orm-model-factory.diag b/graph/test/javascript/expected/74-orm-model-factory.diag index f191f565..f55347e0 100644 --- a/graph/test/javascript/expected/74-orm-model-factory.diag +++ b/graph/test/javascript/expected/74-orm-model-factory.diag @@ -1,5 +1,9 @@ import_cause models.js:21:17 other-orm not_staged import_cause models.js:7:18 mongoose not_staged +library_receiver models.js:12:1 docSchema.static mongoose +library_receiver models.js:13:1 docSchema.static mongoose +library_receiver models.js:15:1 docSchema.method mongoose +library_receiver models.js:28:10 mongoose.model mongoose package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - unresolved models.js:10:19 CONSTRUCTOR_CALL Schema callee_untyped unresolved models.js:11:51 METHOD_CALL find receiver_untyped diff --git a/graph/test/javascript/tools/normalize_diagnostics.py b/graph/test/javascript/tools/normalize_diagnostics.py index 242a8bfd..d7cdfdd3 100644 --- a/graph/test/javascript/tools/normalize_diagnostics.py +++ b/graph/test/javascript/tools/normalize_diagnostics.py @@ -89,6 +89,10 @@ def raw(path, width): for f in raw(os.path.join(out, 'unresolved-value-callee.csv'), 2): pos, _, text = site.get(f[0], ('?', '?', '?')) lines.add('value_callee %s %s %s' % (pos, text, f[1])) +# library_receiver(CallExpr, Specifier): the receiver is a package's value, so a same-named project method is not the callee +for f in raw(os.path.join(out, 'library-receiver.csv'), 2): + pos, _, text = site.get(f[0], ('?', '?', '?')) + lines.add('library_receiver %s %s %s' % (pos, text, f[1])) # package_entry(Prov, Package, Subpath, Condition, Source, Target, Outcome, ModuleHash) for f in raw(os.path.join(out, 'package-entry.csv'), 8): lines.add('package_entry %s %s [%s] %s %s %s -> %s' % ( diff --git a/graph/test/python/cases/43-module-singleton-import/src/api.py b/graph/test/python/cases/43-module-singleton-import/src/api.py new file mode 100644 index 00000000..a169f808 --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/api.py @@ -0,0 +1,7 @@ +"""Imports the singleton whose name collides with its module's last segment.""" + +from order_service import order_service + + +def cancel_endpoint(oid): + return order_service.cancel(oid) diff --git a/graph/test/python/cases/43-module-singleton-import/src/order_service.py b/graph/test/python/cases/43-module-singleton-import/src/order_service.py new file mode 100644 index 00000000..a3ba1433 --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/order_service.py @@ -0,0 +1,20 @@ +"""A module-level singleton named after its own module — the ordinary idiom. + +`from order_service import order_service` must bind the VALUE, not suffix-match +back to this module and report a module import (#1140). +""" + + +class OrderService: + def cancel(self, oid): + return oid + + def refund(self, oid): + return oid + + +order_service = OrderService() + + +def same_module_caller(oid): + return order_service.cancel(oid) diff --git a/graph/test/python/cases/43-module-singleton-import/src/pkg/__init__.py b/graph/test/python/cases/43-module-singleton-import/src/pkg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py b/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py new file mode 100644 index 00000000..a02ce3d5 --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py @@ -0,0 +1,5 @@ +"""Control: a GENUINE submodule import must keep resolving as a module.""" + + +def helper(): + return 1 diff --git a/graph/test/python/cases/43-module-singleton-import/src/publisher.py b/graph/test/python/cases/43-module-singleton-import/src/publisher.py new file mode 100644 index 00000000..4466e43e --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/publisher.py @@ -0,0 +1,5 @@ +from signals import order_placed + + +def publish(sender): + return order_placed.send(sender) diff --git a/graph/test/python/cases/43-module-singleton-import/src/signals.py b/graph/test/python/cases/43-module-singleton-import/src/signals.py new file mode 100644 index 00000000..b5bdb0e8 --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/signals.py @@ -0,0 +1,9 @@ +"""A module-level value whose name does NOT collide with the module name.""" + + +class Signal: + def send(self, sender): + return sender + + +order_placed = Signal() diff --git a/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py b/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py new file mode 100644 index 00000000..027d3e32 --- /dev/null +++ b/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py @@ -0,0 +1,5 @@ +from pkg import tools + + +def run(): + return tools.helper() diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.edges b/graph/test/python/expected/21-url-and-signal-dispatch.edges index 63aeda15..9d07c714 100644 --- a/graph/test/python/expected/21-url-and-signal-dispatch.edges +++ b/graph/test/python/expected/21-url-and-signal-dispatch.edges @@ -1,13 +1,13 @@ ambiguous_unknown DECORATOR_APPLICATION handlers. -> - ambiguous_unknown DECORATOR_APPLICATION main. -> - ambiguous_unknown METHOD_CALL main.not_a_signal_send -> - -ambiguous_unknown METHOD_CALL ship.ship_order -> - boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__ known_edge DECORATOR_CALL handlers. -> handlers.receiver known_edge DECORATOR_CALL main. -> main.receiver known_edge METHOD_CALL main. -> main.Signal.connect known_edge METHOD_CALL main.place_order -> main.Signal.send +known_edge METHOD_CALL ship.ship_order -> signals.Signal.send known_edge SIMPLE_CALL handlers.on_never_shipped -> handlers._record known_edge SIMPLE_CALL handlers.on_shipped -> handlers._record known_edge SIMPLE_CALL main. -> main.path diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.tiers b/graph/test/python/expected/21-url-and-signal-dispatch.tiers index dff4f56f..56e70052 100644 --- a/graph/test/python/expected/21-url-and-signal-dispatch.tiers +++ b/graph/test/python/expected/21-url-and-signal-dispatch.tiers @@ -1,9 +1,9 @@ distinct call sites emitted: 32 --- by tier: edge ROWS, and the distinct SITES they cover --- - 6 rows 6 sites ambiguous_unknown + 5 rows 5 sites ambiguous_unknown 4 rows 4 sites boundary_lib - 22 rows 22 sites known_edge + 23 rows 23 sites known_edge --- edge rows by call kind --- 4 DECORATOR_APPLICATION @@ -13,14 +13,13 @@ distinct call sites emitted: 32 --- unresolved reasons --- 4 decorator_factory_result_untyped - 1 untyped_receiver:local_untyped 1 untyped_receiver:parameter --- the engine's own conservation ledger --- 32 _total_sites - 6 ambiguous_unknown + 5 ambiguous_unknown 4 boundary_lib - 22 known_edge + 23 known_edge --- reconciling rows against the conserved site count --- edge rows 32 diff --git a/graph/test/python/expected/33-url-view-forms.edges b/graph/test/python/expected/33-url-view-forms.edges index f5bb9f3f..c0500600 100644 --- a/graph/test/python/expected/33-url-view-forms.edges +++ b/graph/test/python/expected/33-url-view-forms.edges @@ -1,9 +1,11 @@ ambiguous_unknown DECORATOR_APPLICATION shop.decorators.audited -> - ambiguous_unknown DECORATOR_BARE shop.views. -> - ambiguous_unknown METHOD_CALL shop.urls. -> - -ambiguous_unknown SIMPLE_CALL catalog.urls. -> - -ambiguous_unknown SIMPLE_CALL shop.urls. -> - boundary_lib DECORATOR_CALL shop.decorators.audited -> external:functools.wraps +boundary_lib SIMPLE_CALL catalog.urls. -> external:path +boundary_lib SIMPLE_CALL shop.urls. -> external:include +boundary_lib SIMPLE_CALL shop.urls. -> external:path +boundary_lib SIMPLE_CALL shop.urls. -> external:re_path known_edge DECORATOR_BARE shop.views. -> shop.decorators.audited known_edge SIMPLE_CALL shop.decorators.audited..wrapper -> shop.views.order_history known_edge SIMPLE_CALL shop.views.BaseWidgetView.get -> shop.views._render diff --git a/graph/test/python/expected/33-url-view-forms.tiers b/graph/test/python/expected/33-url-view-forms.tiers index 2713fd47..f05de8d3 100644 --- a/graph/test/python/expected/33-url-view-forms.tiers +++ b/graph/test/python/expected/33-url-view-forms.tiers @@ -1,8 +1,8 @@ distinct call sites emitted: 51 --- by tier: edge ROWS, and the distinct SITES they cover --- - 32 rows 32 sites ambiguous_unknown - 1 rows 1 sites boundary_lib + 6 rows 6 sites ambiguous_unknown + 27 rows 27 sites boundary_lib 18 rows 18 sites known_edge --- edge rows by call kind --- @@ -14,13 +14,13 @@ distinct call sites emitted: 51 --- unresolved reasons --- 1 decorator_factory_result_untyped - 27 no_rule + 1 no_rule 4 unmodelled_decorator --- the engine's own conservation ledger --- 51 _total_sites - 32 ambiguous_unknown - 1 boundary_lib + 6 ambiguous_unknown + 27 boundary_lib 18 known_edge --- reconciling rows against the conserved site count --- diff --git a/graph/test/python/expected/37-model-signals.edges b/graph/test/python/expected/37-model-signals.edges index a50f46f3..67531829 100644 --- a/graph/test/python/expected/37-model-signals.edges +++ b/graph/test/python/expected/37-model-signals.edges @@ -1,6 +1,4 @@ ambiguous_unknown DECORATOR_APPLICATION models. -> - -ambiguous_unknown METHOD_CALL models.Invoice. -> - -ambiguous_unknown METHOD_CALL models.Order. -> - ambiguous_unknown METHOD_CALL views.create_order -> - ambiguous_unknown METHOD_CALL views.drop_invoice -> - ambiguous_unknown METHOD_CALL views.drop_order -> - @@ -9,4 +7,6 @@ ambiguous_unknown METHOD_CALL views.save_draft -> - ambiguous_unknown METHOD_CALL views.touch_invoice -> - boundary_lib DECORATOR_CALL models. -> external:receiver boundary_lib METHOD_CALL models. -> external:post_save.connect +boundary_lib METHOD_CALL models.Invoice. -> external:models.IntegerField +boundary_lib METHOD_CALL models.Order. -> external:models.CharField known_edge METHOD_CALL views.save_cache -> views.Cache.save diff --git a/graph/test/python/expected/37-model-signals.tiers b/graph/test/python/expected/37-model-signals.tiers index a7e08a5c..e87fd3fa 100644 --- a/graph/test/python/expected/37-model-signals.tiers +++ b/graph/test/python/expected/37-model-signals.tiers @@ -1,8 +1,8 @@ distinct call sites emitted: 14 --- by tier: edge ROWS, and the distinct SITES they cover --- - 10 rows 10 sites ambiguous_unknown - 3 rows 3 sites boundary_lib + 8 rows 8 sites ambiguous_unknown + 5 rows 5 sites boundary_lib 1 rows 1 sites known_edge --- edge rows by call kind --- @@ -14,13 +14,12 @@ distinct call sites emitted: 14 2 decorator_factory_result_untyped 4 escape_hatch 1 untyped_receiver:class_attribute_absent - 2 untyped_receiver:local_untyped 1 untyped_receiver:parameter --- the engine's own conservation ledger --- 14 _total_sites - 10 ambiguous_unknown - 3 boundary_lib + 8 ambiguous_unknown + 5 boundary_lib 1 known_edge --- reconciling rows against the conserved site count --- diff --git a/graph/test/python/expected/38-app-wiring.edges b/graph/test/python/expected/38-app-wiring.edges index 89dad32f..39d93d28 100644 --- a/graph/test/python/expected/38-app-wiring.edges +++ b/graph/test/python/expected/38-app-wiring.edges @@ -3,8 +3,6 @@ ambiguous_unknown DECORATOR_ATTRIBUTE main. -> - ambiguous_unknown DECORATOR_BARE main. -> - ambiguous_unknown SIMPLE_CALL main.Timing.dispatch -> - ambiguous_unknown SIMPLE_CALL main.add_header -> - -ambiguous_unknown SIMPLE_CALL shop.jobs.nightly -> - -ambiguous_unknown SIMPLE_CALL shop.jobs.unknown -> - boundary_lib DECORATOR_CALL main. -> external:APIRouter.api_route boundary_lib DECORATOR_CALL main. -> external:FastAPI.exception_handler boundary_lib DECORATOR_CALL main. -> external:FastAPI.middleware @@ -17,6 +15,8 @@ boundary_lib SIMPLE_CALL main. -> external:FastAPI boundary_lib SIMPLE_CALL main. -> external:Route boundary_lib SIMPLE_CALL main. -> external:Starlette boundary_lib SIMPLE_CALL main. -> external:WebSocketRoute +boundary_lib SIMPLE_CALL shop.jobs.nightly -> external:call_command +boundary_lib SIMPLE_CALL shop.jobs.unknown -> external:call_command known_edge METHOD_CALL main. -> main.Registry.add_route known_edge SIMPLE_CALL main. -> main.configure known_edge SIMPLE_CALL shop.management.commands.close_orders.Command.handle -> shop.orders.close_all diff --git a/graph/test/python/expected/38-app-wiring.tiers b/graph/test/python/expected/38-app-wiring.tiers index 4df06d8a..8496a4b9 100644 --- a/graph/test/python/expected/38-app-wiring.tiers +++ b/graph/test/python/expected/38-app-wiring.tiers @@ -1,8 +1,8 @@ distinct call sites emitted: 26 --- by tier: edge ROWS, and the distinct SITES they cover --- - 10 rows 10 sites ambiguous_unknown - 13 rows 13 sites boundary_lib + 8 rows 8 sites ambiguous_unknown + 15 rows 15 sites boundary_lib 3 rows 3 sites known_edge --- edge rows by call kind --- @@ -16,13 +16,12 @@ distinct call sites emitted: 26 --- unresolved reasons --- 2 callee_is_parameter 4 decorator_factory_result_untyped - 2 no_rule 2 unmodelled_decorator --- the engine's own conservation ledger --- 26 _total_sites - 10 ambiguous_unknown - 13 boundary_lib + 8 ambiguous_unknown + 15 boundary_lib 3 known_edge --- reconciling rows against the conserved site count --- diff --git a/graph/test/python/expected/42-signal-forms.edges b/graph/test/python/expected/42-signal-forms.edges index c42f783d..456f0118 100644 --- a/graph/test/python/expected/42-signal-forms.edges +++ b/graph/test/python/expected/42-signal-forms.edges @@ -1,11 +1,11 @@ ambiguous_unknown DECORATOR_APPLICATION handlers. -> - ambiguous_unknown METHOD_CALL handlers. -> - ambiguous_unknown METHOD_CALL orders.close -> - -ambiguous_unknown METHOD_CALL orders.pay -> - -ambiguous_unknown METHOD_CALL orders.place -> - ambiguous_unknown METHOD_CALL orders.ship -> - -ambiguous_unknown METHOD_CALL orders.void -> - boundary_lib DECORATOR_CALL handlers. -> external:receiver boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__ known_edge DECORATOR_CALL handlers. -> handlers.remember known_edge METHOD_CALL orders.not_a_signal -> orders.Outbox.asend +known_edge METHOD_CALL orders.pay -> signals.Signal.asend +known_edge METHOD_CALL orders.place -> signals.Signal.send +known_edge METHOD_CALL orders.void -> signals.Signal.send diff --git a/graph/test/python/expected/42-signal-forms.tiers b/graph/test/python/expected/42-signal-forms.tiers index e28d1d9d..e780b18a 100644 --- a/graph/test/python/expected/42-signal-forms.tiers +++ b/graph/test/python/expected/42-signal-forms.tiers @@ -1,9 +1,9 @@ distinct call sites emitted: 22 --- by tier: edge ROWS, and the distinct SITES they cover --- - 11 rows 11 sites ambiguous_unknown + 8 rows 8 sites ambiguous_unknown 9 rows 9 sites boundary_lib - 2 rows 2 sites known_edge + 5 rows 5 sites known_edge --- edge rows by call kind --- 5 DECORATOR_APPLICATION @@ -14,13 +14,12 @@ distinct call sites emitted: 22 --- unresolved reasons --- 5 decorator_factory_result_untyped 3 untyped_receiver:attribute_object_untyped - 3 untyped_receiver:local_untyped --- the engine's own conservation ledger --- 22 _total_sites - 11 ambiguous_unknown + 8 ambiguous_unknown 9 boundary_lib - 2 known_edge + 5 known_edge --- reconciling rows against the conserved site count --- edge rows 22 diff --git a/graph/test/python/expected/43-module-singleton-import.edges b/graph/test/python/expected/43-module-singleton-import.edges new file mode 100644 index 00000000..c35a14c7 --- /dev/null +++ b/graph/test/python/expected/43-module-singleton-import.edges @@ -0,0 +1,6 @@ +boundary_lib SIMPLE_CALL order_service. -> builtin:object.__init__ +boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__ +known_edge METHOD_CALL api.cancel_endpoint -> order_service.OrderService.cancel +known_edge METHOD_CALL order_service.same_module_caller -> order_service.OrderService.cancel +known_edge METHOD_CALL publisher.publish -> signals.Signal.send +known_edge METHOD_CALL use_pkg.run -> pkg.tools.helper diff --git a/graph/test/python/expected/43-module-singleton-import.entries b/graph/test/python/expected/43-module-singleton-import.entries new file mode 100644 index 00000000..276e39fa --- /dev/null +++ b/graph/test/python/expected/43-module-singleton-import.entries @@ -0,0 +1 @@ +── entry_point (0) ── diff --git a/graph/test/python/expected/43-module-singleton-import.framework b/graph/test/python/expected/43-module-singleton-import.framework new file mode 100644 index 00000000..29577b75 --- /dev/null +++ b/graph/test/python/expected/43-module-singleton-import.framework @@ -0,0 +1,6 @@ +── framework_edge (0) ── +── framework_unjoined (1) ── + 1 signal_dispatch no_receiver +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/43-module-singleton-import.tiers b/graph/test/python/expected/43-module-singleton-import.tiers new file mode 100644 index 00000000..3f658c74 --- /dev/null +++ b/graph/test/python/expected/43-module-singleton-import.tiers @@ -0,0 +1,23 @@ +distinct call sites emitted: 6 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 2 rows 2 sites boundary_lib + 4 rows 4 sites known_edge + +--- edge rows by call kind --- + 4 METHOD_CALL + 2 SIMPLE_CALL + +--- unresolved reasons --- + (none — every site resolved) + +--- the engine's own conservation ledger --- + 6 _total_sites + 2 boundary_lib + 4 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 6 + minus extra rows from multi-target sites 0 + = tier/site pairs 6 + engine's conserved site total 6 diff --git a/graph/test/python/tools/literal_gate.py b/graph/test/python/tools/literal_gate.py index db44b130..0e7af6f7 100644 --- a/graph/test/python/tools/literal_gate.py +++ b/graph/test/python/tools/literal_gate.py @@ -83,7 +83,12 @@ # unresolved_value_callee's second column is `binding`: WHAT the callee of an # unresolved call is ("parameter", "loop_variable", "field", ...), the same words # the JavaScript rules write. Written into the output and never joined on. - "unresolved_value_callee(") + "unresolved_value_callee(", + # lib_callback_edge's How column (receiver / receiver_field / argument / argument_field) says + # where on the call the handed-back object arrived. Written into the output by + # framework-behavior/library-callbacks.dl and never joined on; every name that DOES decide a + # library hand-back comes from the tables graph/python/libsum derives from the library's source. + "lib_callback_edge(", "lcb_hit(") GROUND_FACT = re.compile(r'^[a-z_]+\((?:\s*"[^"]*"\s*,?)+\)\.\s*$') diff --git a/graph/test/python/tools/protocol-fan-test.sh b/graph/test/python/tools/protocol-fan-test.sh index 009607d6..b6c9b283 100755 --- a/graph/test/python/tools/protocol-fan-test.sh +++ b/graph/test/python/tools/protocol-fan-test.sh @@ -64,7 +64,7 @@ PYEOF node "$PARSER" "$W/src" protofan false "$W/ir" > "$W/parse.log" 2>&1 || { echo " FAIL protocol-fan: the parser failed on the fixture"; sed 's/^/ /' "$W/parse.log" | tail -3 echo "protocol-fan: FAILED (1 check)"; exit 1; } -bash "$ROOT/graph/pipeline/run-souffle.sh" --debug --language python --client-ir "$W/ir" \ +bash "$ROOT/graph/pipeline/run-souffle.sh" --debug --language python --dispatch-cap 20 --client-ir "$W/ir" \ --library "$EMPTY_LIB" --intermediate "$W/int" --output "$W/out" > "$W/solve.log" 2>&1 || { echo " FAIL protocol-fan: the solve failed"; tail -3 "$W/solve.log" | sed 's/^/ /' echo "protocol-fan: FAILED (1 check)"; exit 1; } diff --git a/graph/test/python/torture/client/f43_def_rebind.py b/graph/test/python/torture/client/f43_def_rebind.py index 107989af..3a454e7e 100644 --- a/graph/test/python/torture/client/f43_def_rebind.py +++ b/graph/test/python/torture/client/f43_def_rebind.py @@ -89,10 +89,10 @@ def call_branched() -> str: def read_property() -> int: """The getter survives — the control that keeps this rule off property pairs. - EXPECT: miss — `h.value = 5` invokes the SETTER, and the engine emits a - PROPERTY_READ edge for a property read and nothing at all for a property WRITE. - That is a pre-existing gap this fixture happens to expose, not something #383 - changed; the getter edge on the next line is what this family is asserting. + `h.value = 5` invokes the SETTER, and the engine now emits a PROPERTY_WRITE + edge for it beside the PROPERTY_READ for the read; both halves of the pair + are live, which is exactly what this control exists to keep true. The getter + edge on the next line is what this family is asserting. """ h = Holder() h.value = 5 diff --git a/graph/test/python/torture/expected/coverage.txt b/graph/test/python/torture/expected/coverage.txt index 06b88e32..daf7e130 100644 --- a/graph/test/python/torture/expected/coverage.txt +++ b/graph/test/python/torture/expected/coverage.txt @@ -1,4 +1,4 @@ -=== per-family coverage (tier-4, 471 scored sites) === +=== per-family coverage (tier-4, 474 scored sites) === f01 inheritance & MRO links= 10 found= 10 (100.0%) missed= 0 wide= 0 WRONG= 0 f02 callables & closures links= 10 found= 9 (90.0%) missed= 1 wide= 0 WRONG= 0 f03 generics links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0 @@ -38,16 +38,16 @@ f40 data descriptor links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0 f41 class attribute absent links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0 f42 f42 links= 6 found= 6 (100.0%) missed= 0 wide= 0 WRONG= 0 - f43 f43 links= 7 found= 7 (100.0%) missed= 0 wide= 1 WRONG= 0 + f43 f43 links= 10 found= 10 (100.0%) missed= 0 wide= 1 WRONG= 0 nestmod.py nestmod.py links= 1 found= 1 (100.0%) missed= 0 wide= 0 WRONG= 0 pkgmod relative imports (subpackage) links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0 - TOTAL links=434 found=428 (98.6%) missed= 6 wide=37 WRONG= 0 - recall 428/434 = 98.6% of the links that actually ran + TOTAL links=437 found=431 (98.6%) missed= 6 wide=37 WRONG= 0 + recall 431/437 = 98.6% of the links that actually ran wide 37 a member of a SOUND SET that did not run on this pass WRONG 0 a single target asserted as certain that never ran - EXPECTED-MISS cases: {'FOUND': 20, 'MISSED': 24} (a CONCRETE here means a known blind spot closed) + EXPECTED-MISS cases: {'FOUND': 18, 'MISSED': 23} (a CONCRETE here means a known blind spot closed) --- non-concrete sites --- f02_callables.py:31 MISSED true=[('lib', 'callables.py', 22)] engine=[] diff --git a/graph/test/python/torture/expected/torture.edges b/graph/test/python/torture/expected/torture.edges index 4d85cc1a..557d3208 100644 --- a/graph/test/python/torture/expected/torture.edges +++ b/graph/test/python/torture/expected/torture.edges @@ -635,6 +635,7 @@ known_edge PROPERTY_READ f40_data_descriptor.via_data_descriptor -> f40_data_des known_edge PROPERTY_READ f40_data_descriptor.via_delete_descriptor -> f40_data_descriptor.Deletable.__get__ known_edge PROPERTY_READ f40_data_descriptor.via_property -> f40_data_descriptor.Prop.slot known_edge PROPERTY_READ f43_def_rebind.read_property -> f43_def_rebind.Holder.value +known_edge PROPERTY_WRITE f43_def_rebind.read_property -> f43_def_rebind.Holder.value known_edge SELF_CALL f02_callables.HoldsCallables.run -> f02_callables.LocalCallable.__call__ known_edge SELF_CALL f28_await.Registry.into_a_field -> f28_await.Registry.build known_edge SELF_CALL f28_await.Registry.via_self_receiver -> f28_await.Registry.build diff --git a/graph/test/python/torture/expected/torture.oracle b/graph/test/python/torture/expected/torture.oracle index 95fd8d90..4267008a 100644 --- a/graph/test/python/torture/expected/torture.oracle +++ b/graph/test/python/torture/expected/torture.oracle @@ -1 +1 @@ -oracle=634 engine=624 agree=587 missing=47 extra=37 +oracle=634 engine=625 agree=588 missing=46 extra=37 diff --git a/graph/test/tools/engine-package-test.sh b/graph/test/tools/engine-package-test.sh index eec6de84..eaa49139 100755 --- a/graph/test/tools/engine-package-test.sh +++ b/graph/test/tools/engine-package-test.sh @@ -11,6 +11,11 @@ # ───────────────────────────────────────────────────────────────────────────── set -u ROOT="$(d="$(cd "$(dirname "$0")" && pwd)"; while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="$(dirname "$d")"; done; echo "$d")" # the repository root, found by its marker +# This test counts compiles and asserts cache-entry names, and the parallel flavor is a per-machine +# question answered by probing c++ — under the stub c++ the probe and a later real-c++ run can answer +# differently, so prepare and the run after it would disagree on the -par cache name. The flavor is +# pinned serial: what is under test is packaging and caching, not the solve. +export AXIOM_SOLVE_PARALLEL=0 fail=0; bad(){ echo " ✗ $*"; fail=$((fail+1)); } [ -x "$ROOT/node_modules/.bin/tsx" ] || { echo "engine-package: SKIP (no node_modules/.bin/tsx — run npm install)"; exit 0; } W="$(mktemp -d)"; SHADOW=""; trap 'rm -rf "$W" ${SHADOW:+"$SHADOW"}' EXIT @@ -68,7 +73,64 @@ fake="$W/fake-engine" { echo '#!/usr/bin/env bash' echo 'while [ $# -gt 0 ]; do case "$1" in -D) D="$2"; shift 2;; -F) shift 2;; *) shift;; esac; done' cut -f2 "$ROOT/graph/$lang/souffle/export_manifest.tsv" | sed 's|^|: > "$D/|; s|$|"|'; } > "$fake" -mkdir -p "$W/stub" "$W/inc/souffle"; : > "$W/inc/souffle/CompiledSouffle.h"; : > "$W/cc-calls" +mkdir -p "$W/stub" "$W/inc/souffle/utility" "$W/inc/souffle/datastructure"; : > "$W/inc/souffle/CompiledSouffle.h"; : > "$W/cc-calls" +# the stub include dir must carry what souffle_overlay patches (the seqlock fix seds these +# write-entry RMWs, the publication fix anchors on BTree.h's two link stores, and the overlay +# refuses an include dir without either), as a real install's headers do +printf '%s\n' 'version.fetch_or(0x1, std::memory_order_acquire);' \ + 'version.fetch_or(0x1, std::memory_order_acquire);' \ + 'version.fetch_or(0x1, std::memory_order_acquire);' > "$W/inc/souffle/utility/ParallelUtil.h" +cat > "$W/inc/souffle/datastructure/BTree.h" <<'BTREE_STUB' + // move child pointers + if (this->inner) { + // move pointers to sibling + auto* other = static_cast(sibling); + for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) { + other->children[j] = getChildren()[i]; + other->children[j]->parent = other; + other->children[j]->position = static_cast(j); + } + } + + // update number of elements + this->numElements = split_point; + sibling->numElements = maxKeys - split_point - 1; + // link this and the sibling node to new root + this->parent = new_root; + // switch root node + *root = new_root; + keys[pos] = key; + getChildren()[pos + 1] = newNode; + newNode->parent = this; + newNode->position = static_cast(pos) + 1; + // get next pointer + auto next = cur->getChild(idx); + + // get lease on next level + auto next_lease = next->lock.start_read(); +BTREE_STUB +cat > "$W/inc/souffle/datastructure/BTreeDelete.h" <<'BTDEL_STUB' + // get next pointer + auto next = cur->getChild(idx); + + // get lease on next level + auto next_lease = next->lock.start_read(); +BTDEL_STUB +mkdir -p "$W/inc/souffle/utility" +printf '%s\n' '#if _WIN64' '#define __builtin_popcountll __popcnt64' '#else' '#define __builtin_popcountll __popcnt' '#endif' >> "$W/inc/souffle/utility/MiscUtil.h" +printf '%s\n' '#ifdef _WIN32' '#include ' > "$W/inc/souffle/datastructure/PiggyList.h" +cat > "$W/inc/souffle/datastructure/ConcurrentFlyweight.h" <<'FLY_STUB' + const Key& fetch(const lane_id H, const index_type Idx) const { + const auto Lane = Lanes.guard(H); + assert(Idx < SlotCount.load(std::memory_order_relaxed)); + return Slots[Idx]->first; + } +FLY_STUB +cat > "$W/inc/souffle/datastructure/RecordTableImpl.h" <<'REC_STUB' + const RamDomain* unpack(RamDomain Index) const override { + return fetch(Index).data(); + } +REC_STUB { echo '#!/usr/bin/env bash' echo "[ \"\$1\" = --version ] && { echo 'Version: $SOUFFLE_VERSION'; exit 0; }" echo 'while [ $# -gt 0 ]; do case "$1" in -g) : > "$2"; echo "// c++" > "$2"; shift 2;; *) shift;; esac; done'; } > "$W/stub/souffle" @@ -83,6 +145,15 @@ if prep; then [ -x "$W/cache/souffle-engine-$lang-$id" ] || bad "prepare left no binary under the run's cache name (souffle-engine-$lang-${id:0:12}…)" [ "$(calls)" = 1 ] || bad "prepare compiled $(calls) time(s), expected 1" grep -q "engine ready" "$W/log" || bad "prepare did not report the engine ready" + h="$W/cache/include-seqlock-fix-4/souffle/utility/ParallelUtil.h" + grep -q 'memory_order_seq_cst' "$h" 2>/dev/null && ! grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$h" \ + || bad "prepare did not leave the patched overlay header (seqlock fix) in the cache" + b="$W/cache/include-seqlock-fix-4/souffle/datastructure/BTree.h" + [ "$(grep -c 'seqlock-fix-3' "$b" 2>/dev/null)" = "4" ] \ + || bad "prepare did not leave the four publication fences (seqlock-fix-3) in the overlay BTree.h" + f="$W/cache/include-seqlock-fix-4/souffle/datastructure/ConcurrentFlyweight.h" + [ "$(grep -c 'seqlock-fix-4' "$f" 2>/dev/null)" = "1" ] \ + || bad "prepare did not leave the speculative-fetch guard (seqlock-fix-4) in the overlay flyweight" else bad "prepare failed:"; tail -8 "$W/log" | sed 's/^/ /'; fi prep || bad "a second prepare failed" grep -q "reusing cached binary" "$W/log" && [ "$(calls)" = 1 ] || bad "a second prepare compiled again ($(calls) compiles)" diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts new file mode 100644 index 00000000..f78a28dc --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts @@ -0,0 +1,21 @@ +// A module-scope const read through member access, as a call argument, in a template and +// under a type query: every use of CFG / S / TOKENS below binds to THIS file's declaration. +export const CFG = { name: 'a', port: 1 } as const; +export const S = mk({}); +export const TOKENS = { Store: 'store' } as const; +export let counter = 0; +// a const that holds a function is a function: its uses are calls, not reads (control) +export const make = (n: number) => n + 1; + +export function mk(o: object): object { + return o; +} + +export function localUse(): string { + return CFG.name; +} + +export function bump(): void { + counter += 1; + counter = 0; +} diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts new file mode 100644 index 00000000..348509d6 --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts @@ -0,0 +1 @@ +export * from './cfg'; diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts new file mode 100644 index 00000000..97b17b45 --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts @@ -0,0 +1,47 @@ +import { CFG, S, TOKENS, make } from '../src'; + +function use(x: unknown): void {} + +export function viaMember(): void { + use(CFG.name); +} + +export function asArgument(): void { + use(S); +} + +export function inTemplate(): string { + return `${CFG.name}-${TOKENS.Store}`; +} + +export function callsTheFunction(): number { + return make(1); +} + +export function shadowed(): string { + // a local of the same name: the read is the local's, not the module's (control) + const CFG = { name: 'local' }; + return CFG.name; +} + +// a read in a type: at module level it is the module's, in a signature the function's +type T = typeof S; + +export function annotated(x: typeof CFG): string { + return x.name; +} + +// a parameter of the same name: `typeof TOKENS` here is the parameter's type (control) +export function shadowedInType(TOKENS: number): typeof TOKENS { + return TOKENS; +} + +function Route(x: unknown): MethodDecorator { return () => {}; } +function Body(x: unknown): ParameterDecorator { return () => {}; } +class Pipe { constructor(readonly s: unknown) {} } + +export class Handler { + // a read in a decorator is the decorated method's: on the method, and on its parameter + @Route(TOKENS.Store) + handle(@Body(new Pipe(S)) body: unknown): void {} +} diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts b/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts new file mode 100644 index 00000000..74ad08dd --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts @@ -0,0 +1,3 @@ +// The same names, declared again in a sibling package: reads here bind here (control). +export const CFG = { name: 'b', port: 2 } as const; +export const S = {}; diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts b/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts new file mode 100644 index 00000000..90a49c65 --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts @@ -0,0 +1,5 @@ +import { CFG, S } from '../src/settings'; + +export function siblingRead(): unknown { + return [CFG.port, S]; +} diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json b/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json new file mode 100644 index 00000000..65ed3564 --- /dev/null +++ b/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "experimentalDecorators": true, + "noEmit": true + } +} diff --git a/graph/test/typescript/expected/17-constrained-generics.fields b/graph/test/typescript/expected/17-constrained-generics.fields index 51e56615..f0dbf2a4 100644 --- a/graph/test/typescript/expected/17-constrained-generics.fields +++ b/graph/test/typescript/expected/17-constrained-generics.fields @@ -1,3 +1,7 @@ ambiguous_unknown read consumer#ownConstraint(T) -> - ambiguous_unknown read consumer#ownConstraintNamed(T) -> - ambiguous_unknown read curried-local#widest(unknown) -> - +known_edge read consumer#dataFirst() -> consumer#rows +known_edge read consumer#localDataFirst() -> consumer#rows +known_edge read consumer#localWidest() -> consumer#rows +known_edge read consumer#widestFirst() -> consumer#rows diff --git a/graph/test/typescript/expected/17-constrained-generics.fields-oracle b/graph/test/typescript/expected/17-constrained-generics.fields-oracle index 320aafb9..d6d71ba2 100644 --- a/graph/test/typescript/expected/17-constrained-generics.fields-oracle +++ b/graph/test/typescript/expected/17-constrained-generics.fields-oracle @@ -1,7 +1,7 @@ 17-constrained-generics [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 3 resolved 0 (0.0%) - tiers ambiguous_unknown=3 - access read=3 - not scored: 3 rows whose target is not a client declaration + sites 7 resolved 4 (57.1%) + tiers ambiguous_unknown=3 known_edge=4 + access read=7 + not scored: 7 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/36-function-value-containers.fields b/graph/test/typescript/expected/36-function-value-containers.fields index e69de29b..741ac6a1 100644 --- a/graph/test/typescript/expected/36-function-value-containers.fields +++ b/graph/test/typescript/expected/36-function-value-containers.fields @@ -0,0 +1 @@ +known_edge read r229#callObjectLiteral(number) -> r229#handlers diff --git a/graph/test/typescript/expected/36-function-value-containers.fields-oracle b/graph/test/typescript/expected/36-function-value-containers.fields-oracle index 6fb4ee24..152dfcc5 100644 --- a/graph/test/typescript/expected/36-function-value-containers.fields-oracle +++ b/graph/test/typescript/expected/36-function-value-containers.fields-oracle @@ -1,9 +1,9 @@ 36-function-value-containers [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 2 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 2 resolved 2 (100.0%) + tiers known_edge=2 + access read=2 + not scored: 2 rows whose target is not a client declaration MISSING r229#callClassMembers(number) READ Holder#viaField MISSING r229#callObjectLiteral(number) READ r229#viaArrow diff --git a/graph/test/typescript/expected/49-indexed-access-return.fields b/graph/test/typescript/expected/49-indexed-access-return.fields index 64f7525f..3f375c0b 100644 --- a/graph/test/typescript/expected/49-indexed-access-return.fields +++ b/graph/test/typescript/expected/49-indexed-access-return.fields @@ -1,2 +1,5 @@ known_edge read main#getDog() -> Zoo#dog +known_edge read main#getDog() -> main#zoo known_edge read main#getDogPlain() -> Zoo#dog +known_edge read main#getDogPlain() -> main#zoo +known_edge read main#pick(T) -> main#zoo diff --git a/graph/test/typescript/expected/49-indexed-access-return.fields-oracle b/graph/test/typescript/expected/49-indexed-access-return.fields-oracle index 2ff6d466..198a9f5b 100644 --- a/graph/test/typescript/expected/49-indexed-access-return.fields-oracle +++ b/graph/test/typescript/expected/49-indexed-access-return.fields-oracle @@ -1,7 +1,7 @@ 49-indexed-access-return [fields] precision 1.0000 (2 correct, 0 wrong) recall 1.0000 (2 of 2 the compiler resolved) - sites 2 resolved 2 (100.0%) - tiers known_edge=2 - access read=2 - not scored: 0 rows whose target is not a client declaration + sites 5 resolved 5 (100.0%) + tiers known_edge=5 + access read=5 + not scored: 3 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/62-field-access-and-type-use.fields b/graph/test/typescript/expected/62-field-access-and-type-use.fields index 32b614fb..5659500c 100644 --- a/graph/test/typescript/expected/62-field-access-and-type-use.fields +++ b/graph/test/typescript/expected/62-field-access-and-type-use.fields @@ -8,6 +8,7 @@ known_edge read Box#run() -> Box#label known_edge read Box#staticRead() -> Box#static total known_edge read Box#viaInterface(Shaped) -> Shaped#width known_edge read Narrow#own() -> Narrow#width +known_edge read members#Shaped2() -> members#Marker2 known_edge readwrite Box#compound(number) -> Box#width known_edge readwrite Box#decrement() -> Box#hidden known_edge readwrite Box#increment() -> Box#hidden diff --git a/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle b/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle index 5964a26b..e23a07d7 100644 --- a/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle +++ b/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle @@ -1,7 +1,7 @@ 62-field-access-and-type-use [fields] precision 1.0000 (20 correct, 0 wrong) recall 1.0000 (20 of 20 the compiler resolved) - sites 19 resolved 18 (94.7%) - tiers ambiguous_unknown=1 known_edge=18 - access read=11 readwrite=3 write=5 - not scored: 1 rows whose target is not a client declaration + sites 20 resolved 19 (95.0%) + tiers ambiguous_unknown=1 known_edge=19 + access read=12 readwrite=3 write=5 + not scored: 2 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/63-annotated-callable-member.fields b/graph/test/typescript/expected/63-annotated-callable-member.fields index e69de29b..fe88d80b 100644 --- a/graph/test/typescript/expected/63-annotated-callable-member.fields +++ b/graph/test/typescript/expected/63-annotated-callable-member.fields @@ -0,0 +1 @@ +known_edge read members#callVarAmbient() -> members#varAmbient diff --git a/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle b/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle index 8e35ea27..146bd47f 100644 --- a/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle +++ b/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle @@ -1,9 +1,9 @@ 63-annotated-callable-member [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 2 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 1 rows whose target is not a client declaration MISSING members#callPropInline(PropInline) READ PropInline#run MISSING members#callPropNamed(PropNamed) READ PropNamed#run diff --git a/graph/test/typescript/expected/64-array-dispatch.fields b/graph/test/typescript/expected/64-array-dispatch.fields index b1252eaf..12ff9d9f 100644 --- a/graph/test/typescript/expected/64-array-dispatch.fields +++ b/graph/test/typescript/expected/64-array-dispatch.fields @@ -1 +1,4 @@ +known_edge read pipeline#runByName(Node) -> pipeline#byName +known_edge read pipeline#runIndexed(Node) -> pipeline#steps +known_edge read pipeline#runIterated(Node) -> pipeline#steps known_edge read steps#measure(Node) -> { width: number }#width diff --git a/graph/test/typescript/expected/64-array-dispatch.fields-oracle b/graph/test/typescript/expected/64-array-dispatch.fields-oracle index cedeaacf..86bafa38 100644 --- a/graph/test/typescript/expected/64-array-dispatch.fields-oracle +++ b/graph/test/typescript/expected/64-array-dispatch.fields-oracle @@ -1,10 +1,10 @@ 64-array-dispatch [fields] precision 0.0000 (0 correct, 1 wrong) recall 0.0000 (0 of 2 the compiler resolved) - sites 1 resolved 1 (100.0%) - tiers known_edge=1 - access read=1 - not scored: 0 rows whose target is not a client declaration + sites 4 resolved 4 (100.0%) + tiers known_edge=4 + access read=4 + not scored: 3 rows whose target is not a client declaration WRONG steps#measure(Node) READ { width: number }#width MISSING pipeline#runByName(Node) READ pipeline#square MISSING steps#measure(Node) READ steps#width diff --git a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields index e69de29b..5310006c 100644 --- a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields +++ b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields @@ -0,0 +1,3 @@ +known_edge read fewer#callAmbient() -> fewer#ambient +known_edge read fewer#callSiblingsOne() -> fewer#siblings +known_edge read fewer#callSiblingsTwo() -> fewer#siblings diff --git a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle index 0d95ec28..a5e21475 100644 --- a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle +++ b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle @@ -1,8 +1,8 @@ 65-annotated-callable-fewer-parameters [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 1 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 3 rows whose target is not a client declaration MISSING fewer#callHolderRun(Holder) READ Holder#run diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.fields b/graph/test/typescript/expected/69-named-and-inline-handlers.fields index e69de29b..ff8c71ee 100644 --- a/graph/test/typescript/expected/69-named-and-inline-handlers.fields +++ b/graph/test/typescript/expected/69-named-and-inline-handlers.fields @@ -0,0 +1,3 @@ +known_edge read routes#() -> routes#app +known_edge read routes#() -> routes#cache +known_edge read routes#() -> routes#headers diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle b/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle index ba7a4326..f63426fd 100644 --- a/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle +++ b/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle @@ -1,7 +1,7 @@ 69-named-and-inline-handlers [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 4 resolved 4 (100.0%) + tiers known_edge=4 + access read=4 + not scored: 4 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields index 97e25270..c12de5c7 100644 --- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields +++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields @@ -1 +1,2 @@ known_edge read fetcher#get(string,Options) -> Options#fetch +known_edge read pool#main() -> pool#fixed diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle index 91b3cf30..7a227b92 100644 --- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle +++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle @@ -1,8 +1,8 @@ 72-object-literal-and-expression-callees [fields] precision 1.0000 (1 correct, 0 wrong) recall 0.5000 (1 of 2 the compiler resolved) - sites 1 resolved 1 (100.0%) - tiers known_edge=1 - access read=1 - not scored: 0 rows whose target is not a client declaration + sites 2 resolved 2 (100.0%) + tiers known_edge=2 + access read=2 + not scored: 1 rows whose target is not a client declaration MISSING registry#price(number) READ Registry#static format diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges index 8a9a7700..1215ed2b 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.edges +++ b/graph/test/typescript/expected/74-jsx-component-forms.edges @@ -10,6 +10,7 @@ callback_registered FUNCTION_CALL app#() @L4 -> app#() callback_registered FUNCTION_CALL parts#() @L33 -> parts#UserCard({ id: string }) callback_registered FUNCTION_CALL parts#() @L41 -> parts#FieldImpl({ label: string },unknown) callback_registered FUNCTION_CALL parts#() @L73 -> parts#buildDefault() +callback_registered JSX_COMPONENT_CALL app#App() @L13 -> Panel#render() intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L4 -> - known_edge FUNCTION_CALL app#useFactory() @L23 -> parts#()@L22 known_edge FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 -> parts#trackClick(string) diff --git a/graph/test/typescript/expected/74-jsx-component-forms.fields b/graph/test/typescript/expected/74-jsx-component-forms.fields index 1aa6da98..6dcde21d 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.fields +++ b/graph/test/typescript/expected/74-jsx-component-forms.fields @@ -1,6 +1,11 @@ ambiguous_unknown read parts#Badge(?) -> - known_edge read Panel#render() -> Panel#props known_edge read Panel#render() -> { title: string }#title +known_edge read app#App() -> app#Settings +known_edge read app#App() -> parts#Field +known_edge read app#App() -> parts#UserCard +known_edge read app#App() -> parts#ui +known_edge read app#useFactory() -> parts#factory known_edge read parts#Avatar({ src: string }) -> { src: string }#src known_edge read parts#Chip({ text: string }) -> { text: string }#text known_edge read parts#FieldImpl({ label: string },unknown) -> { label: string }#label diff --git a/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle b/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle index da9a8ee2..e2cf577f 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle +++ b/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle @@ -1,10 +1,10 @@ 74-jsx-component-forms [fields] precision 0.2857 (2 correct, 5 wrong) recall 0.2500 (2 of 8 the compiler resolved) - sites 8 resolved 7 (87.5%) - tiers ambiguous_unknown=1 known_edge=7 - access read=7 write=1 - not scored: 1 rows whose target is not a client declaration + sites 13 resolved 12 (92.3%) + tiers ambiguous_unknown=1 known_edge=12 + access read=12 write=1 + not scored: 6 rows whose target is not a client declaration WRONG Panel#render() READ { title: string }#title WRONG parts#Avatar({ src: string }) READ { src: string }#src WRONG parts#Chip({ text: string }) READ { text: string }#text diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields index 3da15a75..82a233a4 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields @@ -2,4 +2,14 @@ ambiguous_unknown read app#(?) -> - ambiguous_unknown read app#(?) -> - ambiguous_unknown read app#(?) -> - ambiguous_unknown read app#() -> - +known_edge read app#Page() -> app#DynAwait +known_edge read app#Page() -> app#DynDefault +known_edge read app#Page() -> app#DynNamed +known_edge read app#Page() -> app#LazyBlock +known_edge read app#Page() -> app#LazyDefault +known_edge read app#Page() -> app#LazyNamed +known_edge read app#Page() -> app#Made +known_edge read app#Page() -> app#Memoed +known_edge read app#Page() -> app#Observed +known_edge read app#Page() -> app#Thing known_edge read ui/Button#Button({ label: string }) -> { label: string }#label diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle index 3617bfdb..aaddbd64 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle @@ -1,9 +1,9 @@ 75-jsx-wrapper-guards [fields] precision 0.0000 (0 correct, 1 wrong) recall 0.0000 (0 of 1 the compiler resolved) - sites 5 resolved 1 (20.0%) - tiers ambiguous_unknown=4 known_edge=1 - access read=5 - not scored: 4 rows whose target is not a client declaration + sites 15 resolved 11 (73.3%) + tiers ambiguous_unknown=4 known_edge=11 + access read=15 + not scored: 14 rows whose target is not a client declaration WRONG ui/Button#Button({ label: string }) READ { label: string }#label MISSING ui/Button#Button({ label: string }) READ ui/Button#label diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields index ce8e4343..da638d2c 100644 --- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields +++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields @@ -1,2 +1,6 @@ ambiguous_unknown read app#(?) -> - ambiguous_unknown read app#useName() -> - +known_edge read app#App() -> app#LazyDefault +known_edge read app#App() -> app#LazyNamedObj +known_edge read app#App() -> app#MemoPicked +known_edge read app#App() -> parts#MemoFwd diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle index c4ca47b6..371b7582 100644 --- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle +++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle @@ -1,7 +1,7 @@ 76-jsx-default-nested-wrappers [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 2 resolved 0 (0.0%) - tiers ambiguous_unknown=2 - access read=2 - not scored: 2 rows whose target is not a client declaration + sites 6 resolved 4 (66.7%) + tiers ambiguous_unknown=2 known_edge=4 + access read=6 + not scored: 6 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/77-package-published-entries.fields b/graph/test/typescript/expected/77-package-published-entries.fields index 0233e076..ccc83f66 100644 --- a/graph/test/typescript/expected/77-package-published-entries.fields +++ b/graph/test/typescript/expected/77-package-published-entries.fields @@ -1,2 +1,3 @@ ambiguous_unknown read index#useStore(number) -> - ambiguous_unknown read plugins/logger#() -> - +known_edge read vanilla#() -> vanilla#DEFAULT_INITIAL diff --git a/graph/test/typescript/expected/77-package-published-entries.fields-oracle b/graph/test/typescript/expected/77-package-published-entries.fields-oracle index 85d59b99..72c9bcde 100644 --- a/graph/test/typescript/expected/77-package-published-entries.fields-oracle +++ b/graph/test/typescript/expected/77-package-published-entries.fields-oracle @@ -1,8 +1,8 @@ 77-package-published-entries [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 1 the compiler resolved) - sites 2 resolved 0 (0.0%) - tiers ambiguous_unknown=2 - access read=2 - not scored: 2 rows whose target is not a client declaration + sites 3 resolved 1 (33.3%) + tiers ambiguous_unknown=2 known_edge=1 + access read=3 + not scored: 3 rows whose target is not a client declaration MISSING index#useStore(number) READ vanilla#value diff --git a/graph/test/typescript/expected/78-callable-collections.fields b/graph/test/typescript/expected/78-callable-collections.fields index b5e742ff..a43f3a76 100644 --- a/graph/test/typescript/expected/78-callable-collections.fields +++ b/graph/test/typescript/expected/78-callable-collections.fields @@ -1,2 +1,17 @@ ambiguous_unknown read pipeline#countIdle() -> - +known_edge read pipeline#() -> pipeline#registry +known_edge read pipeline#() -> pipeline#steps +known_edge read pipeline#countIdle() -> pipeline#idle +known_edge read pipeline#keysOnly() -> pipeline#table +known_edge read pipeline#runByKey(Node,'grow' | 'shrink') -> pipeline#table +known_edge read pipeline#runByName(Node) -> pipeline#table +known_edge read pipeline#runForEach(Node) -> pipeline#steps +known_edge read pipeline#runFrozen(Node) -> pipeline#frozen +known_edge read pipeline#runImported(Node) -> steps#exportedSteps +known_edge read pipeline#runIndexed(Node,number) -> pipeline#steps +known_edge read pipeline#runIterated(Node) -> pipeline#steps +known_edge read pipeline#runReduce(Node) -> pipeline#frozen +known_edge read pipeline#runRegistry(Node) -> pipeline#extended +known_edge read pipeline#runRegistry(Node) -> pipeline#registry +known_edge read pipeline#runValues(Node) -> pipeline#table known_edge read steps#measure(Node) -> { width: number }#width diff --git a/graph/test/typescript/expected/78-callable-collections.fields-oracle b/graph/test/typescript/expected/78-callable-collections.fields-oracle index 8fc5cf9a..0fe6772f 100644 --- a/graph/test/typescript/expected/78-callable-collections.fields-oracle +++ b/graph/test/typescript/expected/78-callable-collections.fields-oracle @@ -1,10 +1,10 @@ 78-callable-collections [fields] precision 0.0000 (0 correct, 1 wrong) recall 0.0000 (0 of 2 the compiler resolved) - sites 2 resolved 1 (50.0%) - tiers ambiguous_unknown=1 known_edge=1 - access read=2 - not scored: 1 rows whose target is not a client declaration + sites 18 resolved 17 (94.4%) + tiers ambiguous_unknown=1 known_edge=17 + access read=18 + not scored: 17 rows whose target is not a client declaration WRONG steps#measure(Node) READ { width: number }#width MISSING pipeline#runByName(Node) READ pipeline#shrink MISSING steps#measure(Node) READ steps#width diff --git a/graph/test/typescript/expected/78-cross-process-destinations.fields b/graph/test/typescript/expected/78-cross-process-destinations.fields index 4fea0459..1d6b842c 100644 --- a/graph/test/typescript/expected/78-cross-process-destinations.fields +++ b/graph/test/typescript/expected/78-cross-process-destinations.fields @@ -1 +1,14 @@ -ambiguous_unknown read client/client#dailyReport(string) -> - +known_edge read client/axios#() -> axios.d#axios +known_edge read client/client#() -> axios.d#axios +known_edge read client/client#audit() -> axios.d#axios +known_edge read client/client#cached() -> client#cache +known_edge read client/client#dailyReport(string) -> axios.d#axios +known_edge read client/client#dailyReport(string) -> config#config +known_edge read client/client#dailyReport(string) -> shared/config#reportsUrl +known_edge read client/client#invoice(string) -> client#billing +known_edge read client/client#listAll() -> paths#ORDERS +known_edge read client/client#placeOrder(unknown) -> axios.d#axios +known_edge read client/client#placeOrder(unknown) -> paths#ORDERS +known_edge read server/orders#() -> orders#app +known_edge read server/orders#() -> orders#router +known_edge read server/orders#() -> paths#ORDERS diff --git a/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle b/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle index bd6298b8..2244ae08 100644 --- a/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle +++ b/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle @@ -1,8 +1,7 @@ 78-cross-process-destinations [fields] - precision 0.0000 (0 correct, 0 wrong) - recall 0.0000 (0 of 1 the compiler resolved) - sites 1 resolved 0 (0.0%) - tiers ambiguous_unknown=1 - access read=1 - not scored: 1 rows whose target is not a client declaration - MISSING client/client#dailyReport(string) READ shared/config#reportsUrl + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 18 resolved 18 (100.0%) + tiers known_edge=18 + access read=18 + not scored: 17 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields index e69de29b..ca48bcea 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields @@ -0,0 +1,12 @@ +known_edge read app#(?) -> rate#RATE +known_edge read app#() -> rate#RATE +known_edge read app#(?) -> rate#RATE +known_edge read app#boot() -> app#host +known_edge read app#boot() -> plugin#plugin +known_edge read app#bootBare() -> app#host +known_edge read app#bootSettings() -> app#host +known_edge read app#bootSettings() -> plugin#settings +known_edge read app#record(number) -> app#seen +known_edge read store#known() -> store#subs +known_edge read store#store() -> store#handlers +known_edge read store#store() -> store#subs diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle index be4f8be6..8c2412a1 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle @@ -1,7 +1,7 @@ 78-hof-callback-at-library-boundary [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 12 resolved 12 (100.0%) + tiers known_edge=12 + access read=12 + not scored: 12 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/79-package-published-members.fields b/graph/test/typescript/expected/79-package-published-members.fields index e69de29b..ec69ab45 100644 --- a/graph/test/typescript/expected/79-package-published-members.fields +++ b/graph/test/typescript/expected/79-package-published-members.fields @@ -0,0 +1,3 @@ +known_edge read src/bound#() -> bound#engine +known_edge read src/index.test#() -> api#api +known_edge read src/index.test#() -> bound#produce diff --git a/graph/test/typescript/expected/79-package-published-members.fields-oracle b/graph/test/typescript/expected/79-package-published-members.fields-oracle index bf1e0d9b..f5234231 100644 --- a/graph/test/typescript/expected/79-package-published-members.fields-oracle +++ b/graph/test/typescript/expected/79-package-published-members.fields-oracle @@ -1,7 +1,7 @@ 79-package-published-members [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 5 resolved 5 (100.0%) + tiers known_edge=5 + access read=5 + not scored: 5 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-generic-return-receiver.fields b/graph/test/typescript/expected/80-generic-return-receiver.fields index b7d27851..e6f9deac 100644 --- a/graph/test/typescript/expected/80-generic-return-receiver.fields +++ b/graph/test/typescript/expected/80-generic-return-receiver.fields @@ -1,2 +1,10 @@ known_edge read Circ#area() -> Circ#r known_edge read Sq#area() -> Sq#s +known_edge read use#() -> use#WARRIOR +known_edge read use#() -> use#container +known_edge read use#ctlAnyIndex(string) -> use#anyBag +known_edge read use#indexValue(string) -> use#shapes +known_edge read use#recordArea(string,number) -> use#makers +known_edge read use#run() -> use#WARRIOR +known_edge read use#run() -> use#container +known_edge read use#totalArea(string,number) -> use#factories diff --git a/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle b/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle index f770da18..09fc58b0 100644 --- a/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle +++ b/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle @@ -1,7 +1,7 @@ 80-generic-return-receiver [fields] precision 1.0000 (2 correct, 0 wrong) recall 1.0000 (2 of 2 the compiler resolved) - sites 4 resolved 4 (100.0%) - tiers known_edge=4 - access read=4 - not scored: 0 rows whose target is not a client declaration + sites 12 resolved 12 (100.0%) + tiers known_edge=12 + access read=12 + not scored: 8 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-object-literal-member-receivers.fields b/graph/test/typescript/expected/80-object-literal-member-receivers.fields index 4b531045..493c648a 100644 --- a/graph/test/typescript/expected/80-object-literal-member-receivers.fields +++ b/graph/test/typescript/expected/80-object-literal-member-receivers.fields @@ -1,3 +1,9 @@ -ambiguous_unknown read local#drive() -> - -ambiguous_unknown read local#trimName() -> - -ambiguous_unknown read use#callNested() -> - +known_edge read local#drive() -> local#handlers +known_edge read local#drive() -> local#jobs +known_edge read local#trimName() -> local#cfg +known_edge read local#trimName() -> local#name +known_edge read use#callGreeter() -> api#greeter +known_edge read use#callNested() -> api#api +known_edge read use#callNested() -> api#users +known_edge read use#callTop() -> api#api +known_edge read use#keepUnrelated() -> use#unrelated diff --git a/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle b/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle index 66475c28..b41e6134 100644 --- a/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle +++ b/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle @@ -1,16 +1,13 @@ 80-object-literal-member-receivers [fields] - precision 0.0000 (0 correct, 0 wrong) - recall 0.0000 (0 of 9 the compiler resolved) - sites 6 resolved 0 (0.0%) - tiers ambiguous_unknown=6 - access read=6 - not scored: 6 rows whose target is not a client declaration - MISSING local#drive() READ local#jobs + precision 1.0000 (3 correct, 0 wrong) + recall 0.3333 (3 of 9 the compiler resolved) + sites 17 resolved 17 (100.0%) + tiers known_edge=17 + access read=17 + not scored: 11 rows whose target is not a client declaration MISSING local#drive() READ local#run - MISSING local#trimName() READ local#name MISSING use#callNested() READ api#byName MISSING use#callNested() READ api#get - MISSING use#callNested() READ api#users MISSING use#callStore() READ api#reset MISSING use#callTop() READ api#find MISSING use#callTop() READ api#remove diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges index 332f0630..15c58497 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges @@ -14,6 +14,7 @@ ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - ambiguous_unknown FUNCTION_CALL reg#() @L18 -> - ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields index e69de29b..d5c739df 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields @@ -0,0 +1,9 @@ +known_edge read app#() -> parts#Child +known_edge read app#() -> parts#FnForm +known_edge read app#() -> parts#NuxtCard +known_edge read app#() -> parts#Panel +known_edge read app#() -> parts#Store +known_edge read app#() -> reg#Button +known_edge read app#() -> reg#Local +known_edge read card#render() -> parts#Child +known_edge read reg#() -> reg#_Local diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle index 7aa79d05..cc14c676 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle @@ -1,7 +1,7 @@ 80-vue-definecomponent-jsx [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 9 resolved 9 (100.0%) + tiers known_edge=9 + access read=9 + not scored: 9 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges index 40aed3c8..92c54f4d 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges @@ -14,6 +14,7 @@ boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent boundary_lib FUNCTION_CALL parts#() @L49 -> vue#makeStore(ComponentOptions) boundary_lib FUNCTION_CALL reg#() @L18 -> vue#defineComponent(ComponentOptions) callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/test/typescript/expected/80-vue-sfc.fields b/graph/test/typescript/expected/80-vue-sfc.fields index 2c586263..7fae20e7 100644 --- a/graph/test/typescript/expected/80-vue-sfc.fields +++ b/graph/test/typescript/expected/80-vue-sfc.fields @@ -1 +1,3 @@ +known_edge read Comp.vue#() -> Comp#total known_edge read child#Child({ msg: string }) -> { msg: string }#msg +known_edge read shims-vue#() -> shims-vue.d#component diff --git a/graph/test/typescript/expected/80-vue-sfc.fields-oracle b/graph/test/typescript/expected/80-vue-sfc.fields-oracle index 706b5d61..4288e38b 100644 --- a/graph/test/typescript/expected/80-vue-sfc.fields-oracle +++ b/graph/test/typescript/expected/80-vue-sfc.fields-oracle @@ -1,9 +1,9 @@ 80-vue-sfc [fields] precision 0.0000 (0 correct, 1 wrong) recall 0.0000 (0 of 1 the compiler resolved) - sites 1 resolved 1 (100.0%) - tiers known_edge=1 - access read=1 - not scored: 0 rows whose target is not a client declaration + sites 4 resolved 4 (100.0%) + tiers known_edge=4 + access read=4 + not scored: 3 rows whose target is not a client declaration WRONG child#Child({ msg: string }) READ { msg: string }#msg MISSING child#Child({ msg: string }) READ child#msg diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields b/graph/test/typescript/expected/81-vue-component-tag.fields index e69de29b..d2a02654 100644 --- a/graph/test/typescript/expected/81-vue-component-tag.fields +++ b/graph/test/typescript/expected/81-vue-component-tag.fields @@ -0,0 +1 @@ +known_edge read shims-vue#() -> shims-vue.d#component diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields-oracle b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle index 7a65896b..21219347 100644 --- a/graph/test/typescript/expected/81-vue-component-tag.fields-oracle +++ b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle @@ -1,7 +1,7 @@ 81-vue-component-tag [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 1 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.fields b/graph/test/typescript/expected/83-closed-world-dispatch.fields index e69de29b..aa725fb8 100644 --- a/graph/test/typescript/expected/83-closed-world-dispatch.fields +++ b/graph/test/typescript/expected/83-closed-world-dispatch.fields @@ -0,0 +1,2 @@ +known_edge read handlers#main() -> handlers#registry +known_edge read handlers#register(HandlerClass) -> handlers#registry diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle b/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle index 3355e740..966f7948 100644 --- a/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle +++ b/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle @@ -1,7 +1,7 @@ 83-closed-world-dispatch [fields] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access - not scored: 0 rows whose target is not a client declaration + sites 2 resolved 2 (100.0%) + tiers known_edge=2 + access read=2 + not scored: 2 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields b/graph/test/typescript/expected/85-workspace-package-import.fields index 565a7ee6..be580e37 100644 --- a/graph/test/typescript/expected/85-workspace-package-import.fields +++ b/graph/test/typescript/expected/85-workspace-package-import.fields @@ -1 +1,3 @@ known_edge read S#run() -> S#bus +known_edge read S#run() -> index#K +known_edge read S#run() -> zod#z diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields-oracle b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle index 44b1f6d7..346e6652 100644 --- a/graph/test/typescript/expected/85-workspace-package-import.fields-oracle +++ b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle @@ -1,7 +1,7 @@ 85-workspace-package-import [fields] precision 1.0000 (1 correct, 0 wrong) recall 1.0000 (1 of 1 the compiler resolved) - sites 1 resolved 1 (100.0%) - tiers known_edge=1 - access read=1 - not scored: 0 rows whose target is not a client declaration + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 2 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.fields b/graph/test/typescript/expected/86-keyed-callback-registry.fields index 02ad1620..7d5b88cf 100644 --- a/graph/test/typescript/expected/86-keyed-callback-registry.fields +++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields @@ -1,12 +1,26 @@ ambiguous_unknown read Bus#count(string) -> - -ambiguous_unknown read Indexer#start() -> - -ambiguous_unknown read app#() -> - -ambiguous_unknown read app#create() -> - -ambiguous_unknown read app#size() -> - known_edge read Bus#count(string) -> Bus#handlers known_edge read Bus#dispatch(string,unknown) -> Bus#handlers +known_edge read Bus#dispatch(string,unknown) -> bus#ANY known_edge read Bus#subscribe(string,Handler) -> Bus#handlers known_edge read Indexer#start() -> Indexer#b +known_edge read Indexer#start() -> topics#TOPIC +known_edge read Indexer#start() -> topics#created +known_edge read Indexer#start() -> topics#removed known_edge read MiniBus#fire(string,unknown) -> MiniBus#h known_edge read MiniBus#on(string,(p: unknown) =) -> MiniBus#h +known_edge read app#() -> app#bus +known_edge read app#() -> app#mini +known_edge read app#() -> bus#ANY +known_edge read app#() -> topics#TOPIC +known_edge read app#() -> topics#created +known_edge read app#create() -> app#bus +known_edge read app#create() -> topics#TOPIC +known_edge read app#create() -> topics#created +known_edge read app#go() -> app#mini +known_edge read app#relay({ type: string }) -> app#bus known_edge read app#relay({ type: string }) -> { type: string }#type +known_edge read app#remove() -> app#bus +known_edge read app#size() -> app#bus +known_edge read app#size() -> topics#TOPIC +known_edge read app#size() -> topics#created diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle b/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle index 9148ff26..f6c67a1e 100644 --- a/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle +++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle @@ -1,14 +1,9 @@ 86-keyed-callback-registry [fields] - precision 0.8571 (6 correct, 1 wrong) - recall 0.5000 (6 of 12 the compiler resolved) - sites 17 resolved 11 (64.7%) - tiers ambiguous_unknown=6 known_edge=11 - access read=17 - not scored: 6 rows whose target is not a client declaration + precision 0.9167 (11 correct, 1 wrong) + recall 0.9167 (11 of 12 the compiler resolved) + sites 36 resolved 35 (97.2%) + tiers ambiguous_unknown=1 known_edge=35 + access read=36 + not scored: 20 rows whose target is not a client declaration WRONG app#relay({ type: string }) READ { type: string }#type - MISSING Indexer#start() READ topics#created - MISSING Indexer#start() READ topics#removed - MISSING app#() READ topics#created - MISSING app#create() READ topics#created MISSING app#relay({ type: string }) READ app#type - MISSING app#size() READ topics#created diff --git a/graph/test/typescript/expected/86-module-variable-reads.edges b/graph/test/typescript/expected/86-module-variable-reads.edges new file mode 100644 index 00000000..c94f0255 --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.edges @@ -0,0 +1,7 @@ +known_edge CONSTRUCTOR_CALL Handler#handle(unknown) @L46 -> Pipe#(unknown) +known_edge DECORATOR_CALL Handler#handle(unknown) @L45 -> a/test/t#Route(unknown) +known_edge DECORATOR_CALL Handler#handle(unknown) @L46 -> a/test/t#Body(unknown) +known_edge FUNCTION_CALL a/src/cfg#() @L4 -> a/src/cfg#mk(object) +known_edge FUNCTION_CALL a/test/t#asArgument() @L10 -> a/test/t#use(unknown) +known_edge FUNCTION_CALL a/test/t#callsTheFunction() @L18 -> a/src/cfg#make(number) +known_edge FUNCTION_CALL a/test/t#viaMember() @L6 -> a/test/t#use(unknown) diff --git a/graph/test/typescript/expected/86-module-variable-reads.entries b/graph/test/typescript/expected/86-module-variable-reads.entries new file mode 100644 index 00000000..9b291f2d --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.entries @@ -0,0 +1,15 @@ +── entry_point (14) ── + exported_from_entry_module a/src/cfg#bump cfg.ts:18 + exported_from_entry_module a/src/cfg#localUse cfg.ts:14 + exported_from_entry_module a/src/cfg#mk cfg.ts:10 + exported_from_entry_module a/test/t#annotated t.ts:30 + exported_from_entry_module a/test/t#asArgument t.ts:9 + exported_from_entry_module a/test/t#callsTheFunction t.ts:17 + exported_from_entry_module a/test/t#inTemplate t.ts:13 + exported_from_entry_module a/test/t#shadowed t.ts:21 + exported_from_entry_module a/test/t#shadowedInType t.ts:35 + exported_from_entry_module a/test/t#viaMember t.ts:5 + exported_from_entry_module b/test/sibling#siblingRead sibling.ts:3 + unimported_module a/src/cfg# cfg.ts:1 + unimported_module a/test/t# t.ts:1 + unimported_module b/test/sibling# sibling.ts:1 diff --git a/graph/test/typescript/expected/86-module-variable-reads.fields b/graph/test/typescript/expected/86-module-variable-reads.fields new file mode 100644 index 00000000..3ea4db04 --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.fields @@ -0,0 +1,21 @@ +ambiguous_unknown read a/test/t#annotated(typeof CFG) -> - +ambiguous_unknown read a/test/t#shadowed() -> - +known_edge read Handler#handle(unknown) -> a/src/cfg#Store +known_edge read Handler#handle(unknown) -> cfg#S +known_edge read Handler#handle(unknown) -> cfg#TOKENS +known_edge read a/src/cfg#localUse() -> a/src/cfg#name +known_edge read a/src/cfg#localUse() -> cfg#CFG +known_edge read a/test/t#() -> cfg#S +known_edge read a/test/t#annotated(typeof CFG) -> cfg#CFG +known_edge read a/test/t#asArgument() -> cfg#S +known_edge read a/test/t#inTemplate() -> a/src/cfg#Store +known_edge read a/test/t#inTemplate() -> a/src/cfg#name +known_edge read a/test/t#inTemplate() -> cfg#CFG +known_edge read a/test/t#inTemplate() -> cfg#TOKENS +known_edge read a/test/t#viaMember() -> a/src/cfg#name +known_edge read a/test/t#viaMember() -> cfg#CFG +known_edge read b/test/sibling#siblingRead() -> b/src/settings#port +known_edge read b/test/sibling#siblingRead() -> settings#CFG +known_edge read b/test/sibling#siblingRead() -> settings#S +known_edge readwrite a/src/cfg#bump() -> cfg#counter +known_edge write a/src/cfg#bump() -> cfg#counter diff --git a/graph/test/typescript/expected/86-module-variable-reads.fields-oracle b/graph/test/typescript/expected/86-module-variable-reads.fields-oracle new file mode 100644 index 00000000..67805e29 --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.fields-oracle @@ -0,0 +1,9 @@ +86-module-variable-reads [fields] + precision 1.0000 (6 correct, 0 wrong) + recall 0.7500 (6 of 8 the compiler resolved) + sites 21 resolved 19 (90.5%) + tiers ambiguous_unknown=2 known_edge=19 + access read=19 readwrite=1 write=1 + not scored: 15 rows whose target is not a client declaration + MISSING a/test/t#annotated(typeof CFG) READ a/src/cfg#name + MISSING a/test/t#shadowed() READ a/test/t#name diff --git a/graph/test/typescript/expected/86-module-variable-reads.oracle b/graph/test/typescript/expected/86-module-variable-reads.oracle new file mode 100644 index 00000000..fa4000b6 --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.oracle @@ -0,0 +1 @@ +oracle=7 engine=7 agree=7 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/86-module-variable-reads.type-use b/graph/test/typescript/expected/86-module-variable-reads.type-use new file mode 100644 index 00000000..a403269b --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.type-use @@ -0,0 +1,5 @@ +ambiguous_unknown AS_TARGET 0 a/src/cfg [EXPRESSION] -> - +ambiguous_unknown AS_TARGET 0 b/src/settings [EXPRESSION] -> - +ambiguous_unknown DECORATOR_TYPE 0 Handler [DECORATOR] -> - +ambiguous_unknown METHOD_RETURN 0 a/test/t [METHOD] -> - +known_edge OBJECT_CREATION_TYPE 0 Handler [EXPRESSION] -> Pipe diff --git a/graph/test/typescript/expected/86-module-variable-reads.types-oracle b/graph/test/typescript/expected/86-module-variable-reads.types-oracle new file mode 100644 index 00000000..e0d90aa4 --- /dev/null +++ b/graph/test/typescript/expected/86-module-variable-reads.types-oracle @@ -0,0 +1,7 @@ +86-module-variable-reads [types] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 8 resolved 1 (12.5%) + tiers ambiguous_unknown=7 known_edge=1 + contexts AS_TARGET=3 DECORATOR_TYPE=2 METHOD_RETURN=2 OBJECT_CREATION_TYPE=1 + not scored: 7 rows whose target is not a client declaration diff --git a/graph/test/typescript/tools/normalize_members.py b/graph/test/typescript/tools/normalize_members.py index 3a47baf3..b6698c7c 100644 --- a/graph/test/typescript/tools/normalize_members.py +++ b/graph/test/typescript/tools/normalize_members.py @@ -25,8 +25,17 @@ def field_names(ir, prefix='typescript'): out = {} types = {t['tsTypeUniqueHash']: (t.get('name') or t.get('qualifiedName') or '?') for t in rows(f'{ir}/all-{prefix}-types.csv')} + mods = {m['tsModuleUniqueHash']: m['qualifiedName'] or m['filePath'] + for m in rows(f'{ir}/all-{prefix}-modules.csv')} for r in rows(f'{ir}/all-{prefix}-fields.csv'): - owner = types.get(r.get('tsTypeLinkHash')) or r.get('ownerTypeName') or '?' + owner = types.get(r.get('tsTypeLinkHash')) + # A key of a module-level `const X = { a: … }` is owned by the variable X (or by the + # key holding a nested literal), which is no type. The compiler side names the nearest + # enclosing named declaration and the module otherwise, so `X.a` is `module#a` there; + # naming it `X#a` here scored every correct read as one wrong plus one missing. + if owner is None and r.get('memberKind') == 'OBJECT_LITERAL_PROPERTY': + owner = mods.get(r.get('tsModuleLinkHash')) + owner = owner or r.get('ownerTypeName') or '?' # `static ` prefixes the NAME, as normalize_edges.Names does for a static method and # as the compiler side's labelOf does. Without it a static member is named one way on # each side and every access to it scores as both a miss and a false positive. @@ -35,6 +44,14 @@ def field_names(ir, prefix='typescript'): return out +def module_var_names(ir, prefix='typescript'): + """variable hash -> (module label, name), for a module-scope variable read by name: its owner is its + module, labelled by the file's stem as the module initializer is (`members#()`).""" + return {r['tsVariableUniqueHash']: (os.path.splitext(os.path.basename(r.get('filePath') or '?'))[0], r['name']) + for r in rows(f'{ir}/all-{prefix}-variables.csv') + if r.get('scopeKind') in ('MODULE_SCOPE', 'GLOBAL_SCOPE', 'AMBIENT_SCOPE', 'NAMESPACE_SCOPE')} + + def type_names(ir, prefix='typescript'): return {t['tsTypeUniqueHash']: (t.get('name') or t.get('qualifiedName') or '?') for t in rows(f'{ir}/all-{prefix}-types.csv')} @@ -53,6 +70,9 @@ def main(): ir, out = args[0], args[1] n = Names(ir, lib_ir) fields = field_names(ir) + # the compiler oracle scores member accesses only: a module variable's read is golden-checked, not oracle-paired + mvars = module_var_names(ir) + fields.update({k: v for k, v in mvars.items() if k not in fields}) types = type_names(ir) if lib_ir and os.path.isdir(lib_ir): fields.update({k: v for k, v in field_names(lib_ir).items() if k not in fields}) @@ -72,7 +92,7 @@ def main(): f"{fields[field][0]}#{fields[field][1]}" if field in fields else f"") if pairs: - if field == '-' or field not in fields: + if field == '-' or field not in fields or field in mvars: continue for d in (['READ'] if access == 'read' else ['WRITE'] if access == 'write' else ['READ', 'WRITE']): diff --git a/graph/typescript/engine/call-edge-generation/call_chain.dl b/graph/typescript/engine/call-edge-generation/call_chain.dl index 56518799..890a08ad 100644 --- a/graph/typescript/engine/call-edge-generation/call_chain.dl +++ b/graph/typescript/engine/call-edge-generation/call_chain.dl @@ -134,6 +134,14 @@ call_chain_edge(ce, caller, "-", m, "client", "callback_registered", kind) :- caller != m, invocation_site(ce, kind). +// …and a function written inside a JSX `{…}` (resolution/value-flow.dl, jsx_handed_value). The `{…}` is +// the site: an intrinsic tag has no call site of its own, and the expression positions the edge. +call_chain_edge(r, caller, "-", m, "client", "callback_registered", "JSX_COMPONENT_CALL") :- + jsx_handed_value(r, m), + method_prov(m, "client"), + jsx_root_caller(r, caller), + caller != m. + // ── call_runs_edge(FromMethod, ToMethod, Prov) ────────────────────────────── // The chain a consumer should WALK: overload signatures re-pointed to the body that // actually executes (resolution/overload-sets.dl). Kept separate from diff --git a/graph/typescript/engine/call-edge-generation/calls.dl b/graph/typescript/engine/call-edge-generation/calls.dl index 959c3c48..b4b87c8f 100644 --- a/graph/typescript/engine/call-edge-generation/calls.dl +++ b/graph/typescript/engine/call-edge-generation/calls.dl @@ -126,3 +126,67 @@ unresolved_covered(ce) :- call_site("client", ck, _, "NONE", _, ce, _), unresolved_receiver(ce, cn, rk, "other_form") :- call_site("client", _, cn, rk, _, ce, _), !call_resolved(ce), !unresolved_covered(ce). + +// ── library_receiver(CallExpr, Specifier) ─────────────────────────────────── +// An unresolved member call whose receiver is a PACKAGE'S value: a binding imported +// from a package no staged IR declares, a member of one, what calling or constructing +// one returns, and a const holding any of those — `request(app).get('/x').expect(200)`, +// `new grpc.Metadata().get(k)`. The receiver's origin is known, so the site is not an +// untyped `x.get()`: a project method of the same name is not what it calls. It stays +// unresolved (the package is not staged), and consumers list it apart from the name +// matches on unknown receivers rather than among them. Read off the source, as the +// JavaScript rule is. A call handed a value of a project type is left out: a wrapper, +// a proxy or a mock helper may return the very object it was given. +package_import(imp, spec) :- import_specifier("client", spec, imp), !import_resolved_module("client", _, imp), + !import_is_type_only("client", imp), import_resolution_kind("client", rk, imp), + rk != "RELATIVE_FILE", rk != "PATHS_ALIAS", !match("[.#~/].*", spec), !match("@/.*", spec). +library_value(e, spec) :- expr_referenced("client", "IMPORT_BINDING", imp, e), package_import(imp, spec). +library_value(e, spec) :- expr_kind("client", k, _, e), (k = "PROPERTY_ACCESS" ; k = "ELEMENT_ACCESS"), + expr_child("client", e, role, _, r), (role = "RECEIVER" ; role = "QUALIFIER"), library_value(r, spec). +library_value(e, spec) :- expr_kind("client", k, _, e), (k = "CALL_EXPRESSION" ; k = "NEW_EXPRESSION"), + expr_child("client", e, "METHOD_NAME", _, c), library_value(c, spec), !call_passes_project_instance(e). +library_value(e, spec) :- expr_kind("client", k, _, e), + (k = "AWAIT_EXPRESSION" ; k = "NON_NULL_EXPRESSION" ; k = "AS_EXPRESSION" ; k = "SATISFIES_EXPRESSION"), + expr_child("client", e, _, _, c), library_value(c, spec). +library_value(e, spec) :- expr_referenced("client", "VARIABLE", v, e), var_initializer("client", _, i, v), + !var_reassigned(v), library_value(i, spec). +call_passes_project_instance(e) :- expr_child("client", e, "ARGUMENT", _, a), expr_type(a, "client", _). +// …and the runtime's own objects: `JSON.parse(s)`, `Promise.resolve(v).then(f)`, `Object.keys(o).forEach(g)`, +// `document.createElement('a')`, `console.error(e)`. With no standard library staged the name resolves to nothing +// in the run (AMBIENT_GLOBAL), so every such call was an untyped `x.parse()` to the consumers, and a project method +// that happens to be named parse, resolve, all, keys, error or getMetadata was given every one of them as a by-name +// caller — and every test around them. The receiver is the platform's, never the project's: a program that declares +// the name itself binds it as its own variable, not as an ambient global, and stays out. Only the objects whose +// members are called through the name; `window`, `globalThis` and `self` are left out, since a program may hang its +// own functions on them. +library_value(e, n) :- expr_referenced("client", "AMBIENT_GLOBAL", _, e), expr_name("client", n, e), + builtin_global_object(n). +builtin_global_object("JSON"). +builtin_global_object("Math"). +builtin_global_object("Object"). +builtin_global_object("Reflect"). +builtin_global_object("Promise"). +builtin_global_object("Array"). +builtin_global_object("Number"). +builtin_global_object("String"). +builtin_global_object("Boolean"). +builtin_global_object("BigInt"). +builtin_global_object("Symbol"). +builtin_global_object("Date"). +builtin_global_object("RegExp"). +builtin_global_object("Error"). +builtin_global_object("Intl"). +builtin_global_object("Atomics"). +builtin_global_object("ArrayBuffer"). +builtin_global_object("console"). +builtin_global_object("process"). +builtin_global_object("Buffer"). +builtin_global_object("document"). +builtin_global_object("navigator"). +builtin_global_object("localStorage"). +builtin_global_object("sessionStorage"). +builtin_global_object("performance"). +builtin_global_object("crypto"). +builtin_global_object("URL"). +library_receiver(ce, spec) :- call_site("client", _, _, rk, recv, ce, _), receiver_kind_is_value(rk), + !call_resolved(ce), library_value(recv, spec). diff --git a/graph/typescript/engine/call-edge-generation/field_access.dl b/graph/typescript/engine/call-edge-generation/field_access.dl index d7d0057f..39396ab7 100644 --- a/graph/typescript/engine/call-edge-generation/field_access.dl +++ b/graph/typescript/engine/call-edge-generation/field_access.dl @@ -86,18 +86,43 @@ field_site_excluded(e, "method_value") :- ts_field_site_is_method(e), !expr_kind("client", "PROPERTY_ACCESS", "METHOD_NAME", e). // ── RESOLUTION — the three receiver-typing paths expr-type.dl already has ─── -ts_field_access_target(e, f) :- property_access_name(e, n), +ts_field_access_typed(e, f) :- property_access_name(e, n), expr_child("client", e, "RECEIVER", _, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f). -ts_field_access_target(e, f) :- property_access_name(e, n), +ts_field_access_typed(e, f) :- property_access_name(e, n), expr_child("client", e, "RECEIVER", _, q), expr_static_type(q, _, qt), field_in_scope(qt, n, "true", f). -ts_field_access_target(e, f) :- property_access_name(e, n), +ts_field_access_typed(e, f) :- property_access_name(e, n), expr_child("client", e, "RECEIVER", _, q), expr_shape(q, qs), field_in_scope(qs, n, _, f). +ts_field_access_target(e, f) :- ts_field_access_typed(e, f). +ts_field_access_is_typed(e) :- ts_field_access_typed(e, _). + +// ── A KEY OF A MODULE-LEVEL CONST TABLE — `TOPICS.orderEvents` ────────────── +// `export const TOPICS = { orderEvents: '…' } as const`: the parser declares each key +// as an OBJECT_LITERAL_PROPERTY owned by the VARIABLE (a nested literal's keys by the +// property holding it). The owner is no type, so none of the three joins above can +// reach it; the receiver names the owner instead — the variable itself, an import of +// it, a namespace import's export of it, or a key of an outer table. Only where the +// typed lookup found nothing: `{ … } satisfies Config` is read as Config's member. +ts_objlit_owner(q, v) :- expr_referenced("client", "VARIABLE", v, q). +ts_objlit_owner(q, v) :- expr_referenced("client", "IMPORT_BINDING", ih, q), + import_binds(ih, _, "VARIABLE", v). +ts_objlit_owner(q, v) :- property_access_name(q, n), + expr_child("client", q, "RECEIVER", _, r), + expr_namespace(r, _, m), + module_export(_, m, n, "VARIABLE", v). +ts_objlit_owner(q, pf) :- ts_field_access_target(q, pf), + field_kind("client", "OBJECT_LITERAL_PROPERTY", _, _, pf). +ts_field_access_target(e, f) :- property_access_name(e, n), + expr_child("client", e, "RECEIVER", _, q), + ts_objlit_owner(q, o), + field_decl("client", n, _, o, f), + field_kind("client", "OBJECT_LITERAL_PROPERTY", _, _, f), + !ts_field_access_is_typed(e). // ── ACCESS DIRECTION ──────────────────────────────────────────────────────── // property_written and property_write_only are accessors.dl's, and are reused rather @@ -124,12 +149,39 @@ ts_field_access_class(e, "ambiguous_unknown") :- ts_field_access_count(e, 0). // The enclosing function of the ACCESS, by expr_enclosing_method, exactly as accessors.dl // attributes an accessor invocation: there is no call site to attribute through. Top-level // code is the module initializer, which that relation already supplies. +// A read inside a METHOD'S DECORATOR is that method's: `@EventPattern(TOPICS.orderEvents)` +// binds the handler to the topic, and the module initializer that evaluates the +// decorator is no user of it. A decorator's expressions carry no owner hash, so the +// subtree is walked down from the decorator's own expression. +ts_deco_expr_method(e, m) :- decorator_expr("client", e, d), decorator_target("client", _, m, d), m != "". +// ...and so is a read in a PARAMETER'S decorator: `constructor(@inject(TOKENS.Store) s)` +// binds the constructor to the token. The decorator's owner is the parameter. +ts_deco_expr_method(e, m) :- decorator_expr("client", e, d), + ts_decorator(_, _, "PARAMETER_DECLARATION", p, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, d), + param_decl("client", _, _, _, m, p), m != "". +ts_deco_expr_method(c, m) :- ts_deco_expr_method(p, m), expr_child("client", p, _, _, c). +ts_field_access_in_decorator(e) :- ts_deco_expr_method(e, _). +// A read in a parameter's DEFAULT is its method's too: `constructor(topic = TOPICS.payments)` +// is evaluated on every call that omits the argument, and the extractor places it in the +// module initializer. Only there: a function written as the default keeps its own reads. +ts_param_default_expr(e, m) :- ts_method_parameter(_, _, m, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e, _, _, _, _), + e != "", m != "". +ts_param_default_expr(c, m) :- ts_param_default_expr(p, m), expr_child("client", p, _, _, c). +ts_field_access_in_function(e) :- expr_enclosing_method(e, m), m != "", !module_init("client", m, _). +ts_param_default_method(e, m) :- ts_param_default_expr(e, m), + !ts_field_access_in_function(e), !ts_field_access_in_decorator(e). ts_field_access_named(e) :- expr_enclosing_method(e, m), m != "". -ts_field_access_from(e, m) :- ts_field_access_site(e), expr_enclosing_method(e, m), m != "". -// ...and the MODULE itself for the residue expr_enclosing_method cannot place, which is -// the marker call_sites.caller_id already uses in TypeScript rather than a blank. Never -// NULL, so a site can always be attributed to the code that contains it. -ts_field_access_from(e, mod) :- ts_field_access_site(e), !ts_field_access_named(e), +ts_field_access_placed(e) :- ts_field_access_named(e). +ts_field_access_placed(e) :- ts_field_access_in_decorator(e). +ts_field_access_placed(e) :- ts_param_default_method(e, _). +ts_field_access_from(e, m) :- ts_field_access_site(e), ts_deco_expr_method(e, m). +ts_field_access_from(e, m) :- ts_field_access_site(e), ts_param_default_method(e, m). +ts_field_access_from(e, m) :- ts_field_access_site(e), expr_enclosing_method(e, m), m != "", + !ts_field_access_in_decorator(e), !ts_param_default_method(e, _). +// ...and the MODULE itself for the residue none of those can place, which is the marker +// call_sites.caller_id already uses in TypeScript rather than a blank. Never NULL, so a +// site can always be attributed to the code that contains it. +ts_field_access_from(e, mod) :- ts_field_access_site(e), !ts_field_access_placed(e), expr_module("client", mod, e), mod != "". // ── THE EXPORTED ROW — field_access(Site, Caller, Field, Prov, Tier, Access) ─ @@ -150,3 +202,88 @@ ts_field_access_has_row(e) :- ts_field_access_target(e, f), ts_field_prov(_, f). field_access(e, caller, "-", "-", "ambiguous_unknown", acc) :- ts_field_access_site(e), !ts_field_access_has_row(e), ts_field_access_from(e, caller), ts_field_access_kind(e, acc). + +// ── A MODULE-SCOPE VARIABLE, READ BY NAME ─────────────────────────────────── +// `SERVICE.name`, `f(EnvSchema)`, `TOKENS.IntentStore`: the receiver or argument is an +// IDENTIFIER_REFERENCE that the binder already tied to one declaration -- a module-scope +// variable of its own file (VARIABLE), or an import whose binding import_binds follows, +// through every re-export, to the exported variable (IMPORT_BINDING). Nothing recorded +// that tie as a data edge, so "who reads this exported const" was answered by name, and +// the name matched every same-named const in every sibling package. +// +// The variable is the field of the row: a module is the owner of its variables the way a +// class is the owner of its fields, and consumers read one relation for both. A variable +// that holds a function is a function (`const f = () => ...`): its uses are calls, which +// call_edges already carries, so it is left out here. +ts_module_var(v) :- var_decl("client", _, _, sk, _, _, v), scope_is_module_level(sk), + !var_is_function(v, _). +ts_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, e), + expr_referenced("client", "VARIABLE", v, e), ts_module_var(v). +ts_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, e), + expr_referenced("client", "IMPORT_BINDING", ih, e), + import_binds(ih, "client", "VARIABLE", v), ts_module_var(v). +// the direction, by the same law property_written applies to a member +ts_var_written(e) :- ts_var_read_target(e, _), expr_child("client", asg, "LEFT_OPERAND", _, e), + expr_kind("client", ak, _, asg), assignment_kind(ak). +ts_var_written(e) :- ts_var_read_target(e, _), expr_child("client", u, "UNARY_OPERAND", _, e), + expr_kind("client", "UNARY_EXPRESSION", _, u), expr_operator("client", op, u), update_operator(op). +ts_var_write_only(e) :- ts_var_read_target(e, _), expr_child("client", asg, "LEFT_OPERAND", _, e), + expr_kind("client", "ASSIGNMENT_EXPRESSION", _, asg). +ts_var_read_kind(e, "write") :- ts_var_written(e), ts_var_write_only(e). +ts_var_read_kind(e, "readwrite") :- ts_var_written(e), !ts_var_write_only(e). +ts_var_read_kind(e, "read") :- ts_var_read_target(e, _), !ts_var_written(e). +// the caller, as for a property access: the enclosing function, else the module. +// A DECORATOR on a method or on one of its parameters is that method's: `place(@Payload(new +// Pipe(CommandSchema)) c)` reads CommandSchema for place(). The parser gives an expression +// inside a decorator no owner hash, and expr_enclosing_method routes it to the module +// initializer, so the read is found by walking down from the decorator's own expression. +ts_decorated_method(dx, m) :- decorator_expr("client", dx, d), + annotation_on("client", _, _, "METHOD_DECLARATION", m, d), m != "". +ts_decorated_method(dx, m) :- decorator_expr("client", dx, d), + annotation_on("client", _, _, "PARAMETER_DECLARATION", p, d), param_decl("client", _, _, _, m, p), m != "". +ts_under_decorator(dx, dx) :- ts_decorated_method(dx, _). +ts_under_decorator(dx, c) :- ts_under_decorator(dx, p), expr_child("client", p, _, _, c). +ts_var_read_decorated(e, m) :- ts_var_read_target(e, _), expr_owner("client", "DECORATOR", _, _, e), + ts_under_decorator(dx, e), ts_decorated_method(dx, m). +ts_var_read_named(e) :- ts_var_read_decorated(e, _). +ts_var_read_named(e) :- ts_var_read_target(e, _), expr_enclosing_method(e, m), m != "". +ts_var_read_from(e, m) :- ts_var_read_decorated(e, m). +ts_var_read_from(e, m) :- ts_var_read_target(e, _), !ts_var_read_decorated(e, _), + expr_enclosing_method(e, m), m != "". +ts_var_read_from(e, mod) :- ts_var_read_target(e, _), !ts_var_read_named(e), + expr_module("client", mod, e), mod != "". +// one declaration, bound by the binder: known_edge +field_access(e, caller, v, "client", "known_edge", acc) :- ts_var_read_target(e, v), + ts_var_read_from(e, caller), ts_var_read_kind(e, acc). + +// ── THE SAME VARIABLE, READ IN A TYPE: `typeof X` ─────────────────────────── +// `type OrderDto = z.infer` depends on OrderSchema exactly as +// `f(OrderSchema)` does, but the name sits in a TYPE_QUERY type reference, not in an +// expression, so the binder's expression tie above never sees it. The name is bound +// through the module's own scope (name_binds_any: a declaration of this file, or a named +// import followed to the exported variable). Only the bare form `typeof X`: in +// `typeof A.b` the variable read is A, and the reference names b. A parameter or a local +// of the same name in the enclosing callable shadows the module's (control). +// The site is the type reference; the bundle positions it from the type-reference table. +ts_type_query_site(ref, tn, mod) :- type_ref("client", "TYPE_QUERY", _, tn, ctn, _, _, ref), + tn != "", ctn = cat("typeof ", tn), type_ref_module("client", mod, ref). +ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, m, "METHOD", ref). +ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, p, "METHOD_PARAM", ref), + param_decl("client", _, _, _, m, p). +ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, lv, "VARIABLE", ref), + var_decl("client", _, _, _, m, _, lv), m != "". +ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, x, "EXPRESSION", ref), + expr_enclosing_method(x, m), m != "". +ts_type_query_shadowed(ref) :- ts_type_query_site(ref, tn, _), ts_type_query_method(ref, m), + param_decl("client", tn, _, _, m, _). +ts_type_query_shadowed(ref) :- ts_type_query_site(ref, tn, _), ts_type_query_method(ref, m), + var_decl("client", tn, _, _, m, _, _). +ts_type_query_read(ref, v) :- ts_type_query_site(ref, tn, mod), !ts_type_query_shadowed(ref), + name_binds_any(mod, tn, "client", "VARIABLE", v), ts_module_var(v). +ts_type_query_has_method(ref) :- ts_type_query_method(ref, _). +field_access(ref, m, v, "client", "known_edge", "read") :- ts_type_query_read(ref, v), + ts_type_query_method(ref, m). +// a type alias, an interface, a class member's annotation: the module's initializer, as for +// any module-level read +field_access(ref, init, v, "client", "known_edge", "read") :- ts_type_query_read(ref, v), + !ts_type_query_has_method(ref), ts_type_query_site(ref, _, mod), module_init("client", init, mod). diff --git a/graph/typescript/engine/config-resolution/entry-points.dl b/graph/typescript/engine/config-resolution/entry-points.dl index 168ff32a..638f4121 100644 --- a/graph/typescript/engine/config-resolution/entry-points.dl +++ b/graph/typescript/engine/config-resolution/entry-points.dl @@ -216,9 +216,7 @@ entry_point(m, "package_export") :- cfg_published_value(x), method_prov(m, "client"). // an instance method read as a value: `engine.produce`, typed through its receiver entry_point(m, "package_export") :- cfg_published_value(x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_type(q, _, t), method_in_scope(t, n, _, m), method_prov(m, "client"). diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index 6fe5e680..c4257044 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -396,15 +396,11 @@ expr_call_candidate(ce, m) :- callee_branch(ce, x), expr_shape(x, s), call_signature_in_scope(s, m). expr_call_candidate(ce, m) :- callee_branch(ce, x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_type(q, _, qt), field_is_callable(qt, n, m). expr_call_candidate(ce, m) :- callee_branch(ce, x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_shape(q, s), field_is_callable(s, n, m). diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index c049ce6d..d8b6b23b 100644 --- a/graph/typescript/engine/expression-resolution/expr-type.dl +++ b/graph/typescript/engine/expression-resolution/expr-type.dl @@ -158,9 +158,7 @@ expr_type(e, "client", t) :- expr_referenced("client", "NAMESPACE", t, e), // // Recurses on itself for free: the rule types `Local.Deep` from `Local`, and a third // segment types from that, so depth is not bounded here. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), namespace_member(prov, qt, n, "TYPE", t). @@ -381,9 +379,7 @@ expr_awaited_type(e, prov, t) :- expr_referenced("client", "VARIABLE", v, e), expr_awaited_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p), ref_awaited_target(r, prov, t). -expr_awaited_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_awaited_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f), @@ -401,26 +397,32 @@ property_access_name(e, n) :- expr_kind("client", "PROPERTY_ACCESS", _, e), expr_name("client", n, pn), n != "". +// ── property_access_recv(ExprHash, Name, ReceiverHash) ──────────────────── +// A property access, decomposed ONCE, below the typing fixpoint: the access, its +// name, its receiver. These same three atoms sat inline in 22 rules, most of them +// recursive through expr_type(q, …), and semi-naive evaluation re-anchored the +// PROPERTY_ACCESS scan every iteration — the souffle profile put expr_type's clauses +// at 17K tuples/s with all of the time recursive, on an answer that is pure syntax +// and never changes. Hoisted, each of those rules starts from its recursive delta +// and probes this table by receiver. +property_access_recv(e, n, q) :- expr_kind("client", "PROPERTY_ACCESS", _, e), + property_access_name(e, n), + expr_child("client", e, "RECEIVER", _, q). + // ── PROPERTY ACCESS — `a.b`, where the parser had nothing ────────────────── // The 123k UNKNOWN references. The qualifier is typed first, then the member is // looked up on it, and the member's own declared type becomes the access's type. // Both member tables are searched by the shape of the qualifier: a VALUE qualifier // searches instance members, a TYPE qualifier (a class name) searches statics. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type(f, prov, t). -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_static_type(q, _, qt), field_in_scope(qt, n, "true", f), field_type(f, prov, t). -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_shape(q, qs), field_in_scope(qs, n, _, f), field_type(f, prov, t). @@ -440,17 +442,13 @@ expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // method_return_type / _shape / _element already describe it, exactly as they describe // the result of a call. This only supplies the missing link from the access to the // member. -property_access_getter(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +property_access_getter(e, m) :- property_access_recv(e, n, q), expr_type(q, _, qt), method_in_scope(qt, n, "false", m), method_kind(_, "GETTER", _, m). // A STATIC getter, reached through a class name rather than a value, mirroring the // static field rule above. -property_access_getter(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +property_access_getter(e, m) :- property_access_recv(e, n, q), expr_static_type(q, _, qt), method_in_scope(qt, n, "true", m), method_kind(_, "GETTER", _, m). @@ -462,29 +460,21 @@ expr_shape(e, s) :- property_access_getter(e, m), expr_element_type(e, prov, t) :- property_access_getter(e, m), method_return_element(m, prov, t). -expr_shape(e, s) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_shape(e, s) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_shape(f, s). // A property access on a NAMESPACE — `fs.promises`, `ts.SyntaxKind`. The member // comes from the module's export table, not from a member table. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, mprov, m), module_export(mprov, m, n, "VARIABLE", v), var_type(v, prov, t). -expr_static_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_static_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, prov, m), module_export(prov, m, n, "TYPE", t). -expr_callable(e, cm) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_callable(e, cm) :- property_access_recv(e, n, q), expr_namespace(q, _, m), module_export(_, m, n, "METHOD", cm). @@ -500,9 +490,7 @@ expr_callable(e, cm) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // same fixpoint, so reading it here would put that negation inside its own cycle. // The ancestor walk is the one method_in_scope (2) performs, with the shadowing // check expressed against the static table directly for the same reason. -expr_callable(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_callable(e, m) :- property_access_recv(e, n, q), expr_static_type(q, _, t), static_member_value(t, n, m). static_member_value(t, n, m) :- scope_sibling(t, sib), @@ -517,9 +505,7 @@ static_member_declared(t, n) :- scope_sibling(t, sib), // A property access on a namespace TYPE — `N.Inner` where N is a namespace // declaration rather than an imported module. -expr_static_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_static_type(e, prov, t) :- property_access_recv(e, n, q), expr_static_type(q, prov, ns), namespace_member(prov, ns, n, "TYPE", t). @@ -551,9 +537,7 @@ expr_element_shape(e, sh) :- expr_call_candidate(e, callee), expr_element_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p), ref_element_target(r, prov, t). -expr_element_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_element_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f), diff --git a/graph/typescript/engine/resolution/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl index c9f675e1..99b05b5a 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -278,6 +278,20 @@ builtin_elem_ref(parent, x) :- ref_is_readonly_operator(parent), builtin_elem_ref(child, x). builtin_elem_ref(ref, x) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x). +// A tuple's elements are its members: `[Module, ...Module[]]`. +builtin_elem_ref(ref, child) :- type_ref(rp, "TUPLE", _, _, _, _, _, ref), + type_ref_parent(rp, ref, _, child). +// A TYPE VARIABLE constrained to a list holds that list's elements: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, as for a `` receiver. Both routes +// ref_type_target takes: the exact link to the declaration, and the name in scope where it is absent. +builtin_elem_ref(ref, x) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + builtin_elem_ref(cref, x). +builtin_elem_ref(ref, x) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + builtin_elem_ref(cref, x). builtin_elem_ref(ref, a) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x), type_ref(_, "TYPE_VARIABLE", _, n, _, _, _, x), @@ -339,6 +353,13 @@ lambda_param_ref(lp, c) :- call_site("client", _, "then", _, recv, ce, _), !param_type_ref(_, _, lp). lambda_param_type(lp, prov, t) :- lambda_param_ref(lp, c), ref_type_target(c, prov, t). +// …and its SHAPE, where the element is an object type written as a type alias or a literal rather +// than a class or an interface: `type Plugin = { init(): void }`, then `plugins.map((p) => p.init())`. +// The element has no type declaration to dispatch on, only members, so the type clause above gave +// the parameter nothing and every call on it was unresolved; an annotated parameter of the same type +// resolved, through param_shape_target. The element reference is the same one, read as a shape. +lambda_param_shape(lp, sh) :- lambda_param_ref(lp, c), + ref_shape_target(c, sh). // The parameter's reference is its declared reference, as an annotation's would be. expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e), lambda_param_ref(p, c). diff --git a/graph/typescript/engine/resolution/generics.dl b/graph/typescript/engine/resolution/generics.dl index 1d11e6a3..77f3f519 100644 --- a/graph/typescript/engine/resolution/generics.dl +++ b/graph/typescript/engine/resolution/generics.dl @@ -48,9 +48,7 @@ expr_decl_ref(e, r) :- expr_referenced("client", "VARIABLE", v, e), var_type_ref(_, r, v). expr_decl_ref(e, r) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p). -expr_decl_ref(e, r) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_decl_ref(e, r) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f). diff --git a/graph/typescript/engine/resolution/name-resolution.dl b/graph/typescript/engine/resolution/name-resolution.dl index afd1a6fc..10dc942f 100644 --- a/graph/typescript/engine/resolution/name-resolution.dl +++ b/graph/typescript/engine/resolution/name-resolution.dl @@ -109,9 +109,13 @@ global_name_demanded("String"). // ReadonlyArray's — `slice`, `every`, `map`, `filter`, `join`, `reduce`, `forEach`. global_name_demanded("ReadonlyArray"). -name_binds_any(mod, name, prov, k, ent) :- module_decl("client", _, _, _, _, mod), - global_name_demanded(name), - global_decl_visible(mod, name, k, prov, ent), +// ANCHORED ON global_decl_visible, which already holds exactly the (module, demanded +// name) pairs with a global to offer — its own clauses carry the module_decl and +// global_name_demanded gates. Written with the cross pair first, the plan enumerated +// |client modules| x |demanded names| and probed global_decl_visible per pair: 30s +// between this rule and name_binds' twin on a 1.1M-LOC subject, for 217K rows. +name_binds_any(mod, name, prov, k, ent) :- global_decl_visible(mod, name, k, prov, ent), + module_decl("client", _, _, _, _, mod), !name_bound_locally(mod, name). // ── name_binds(ModuleHash, Name, Prov, Kind, EntityHash) ──────────────────── @@ -125,9 +129,8 @@ name_binds(mod, local, prov, k, ent) :- import_binding("client", _, local, _, mo import_binds(ih, prov, k, ent). name_binds(mod, name, prov, k, ent) :- module_local_decl(prov, mod, name, k, ent), !decl_is_type_only(prov, k, ent). -name_binds(mod, name, prov, k, ent) :- module_decl("client", _, _, _, _, mod), - global_name_demanded(name), - global_decl_visible(mod, name, k, prov, ent), +name_binds(mod, name, prov, k, ent) :- global_decl_visible(mod, name, k, prov, ent), + module_decl("client", _, _, _, _, mod), !decl_is_type_only(prov, k, ent), !name_bound_locally(mod, name). @@ -210,13 +213,16 @@ qualified_name_binds(mod, ctn, prov, k, ent) :- qname_walk(mod, ctn, ctn, prov, // A namespace IMPORT qualifier — `import * as ts from "typescript"; ts.SourceFile`. // The leftmost segment is a module rather than a namespace declaration, so the // member comes from that module's export table instead of from namespace_member. -qualified_name_binds(mod, ctn, prov, k, ent) :- demanded_qualified_name(ctn), +// The qualifier is matched as an EQUALITY KEY, not as a substring test. An import's +// local name holds no dot, so "the demanded name starts with local followed by a dot" +// is exactly "local is one of the demanded name's dot-prefixes" — and qname_dot_prefix +// already enumerates those. Written with the substring test, no index applies and the +// body is |demanded names| x |import bindings| with two substr calls per pair: 29s for +// 805 rows on a 1.1M-LOC subject, the single most expensive rule of the solve. +qualified_name_binds(mod, ctn, prov, k, ent) :- qname_dot_prefix(ctn, local), import_binding("client", _, local, _, mod, ih), import_binds_namespace_any(ih, prov, target), l = strlen(local), - l < strlen(ctn), - substr(ctn, 0, l) = local, - substr(ctn, l, 1) = ".", seg = substr(ctn, l + 1, strlen(ctn) - l - 1), module_export_any(prov, target, seg, k, ent). @@ -236,12 +242,16 @@ qualified_name_binds(mod, ctn, prov, k, ent) :- demanded_qualified_name(ctn), // needed: qname_dot_prefix already enumerates the prefixes, and a prefix that is the // qualifier plus one segment has no interior dot by construction — the next prefix out // would be the one that contains it. +// `local` is joined as another dot-prefix of the SAME demanded name: two prefixes of +// one string nest, so "local is a shorter prefix of dotted ending at a dot" is implied +// by both being dot-prefixes of ctn with strlen(local) < strlen(dotted) — the substring +// tests the first version ran per (binding x prefix) pair are gone, and import_binding +// is probed by local through its index. qname_walk(mod, ctn, dotted, prov, k, ent) :- qname_dot_prefix(ctn, dotted), - import_binding("client", _, local, _, mod, ih), - import_binds_namespace_any(ih, prov, target), + qname_dot_prefix(ctn, local), l = strlen(local), l < strlen(dotted), - substr(dotted, 0, l) = local, - substr(dotted, l, 1) = ".", + import_binding("client", _, local, _, mod, ih), + import_binds_namespace_any(ih, prov, target), seg = substr(dotted, l + 1, strlen(dotted) - l - 1), module_export_any(prov, target, seg, k, ent). diff --git a/graph/typescript/engine/resolution/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl index 2c8e5ef2..0d3cff9f 100644 --- a/graph/typescript/engine/resolution/type-resolution.dl +++ b/graph/typescript/engine/resolution/type-resolution.dl @@ -587,6 +587,19 @@ ref_element_target(ref, prov, t) :- type_ref(rp, k, _, _, _, _, _, ref), ref_kind_is_array(k), type_ref_parent(rp, ref, _, child), ref_type_target(child, prov, t). +// …and of a TYPE VARIABLE constrained to an array or a tuple: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, exactly as it is for a `` receiver +// (ref_type_target above), so the callback's parameter is typed by the constraint's element. Without it +// the element of a generic list had no type, and every call on it was unresolved. Same two routes as +// ref_type_target: the exact link to the declaration, and the name in scope where the link is absent. +ref_element_target(ref, prov, t) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + ref_element_target(cref, prov, t). +ref_element_target(ref, prov, t) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + ref_element_target(cref, prov, t). // ── ref_element_shape(Ref, Shape) — an element that is ITSELF a function type ── // `fns: readonly ((data: T) => boolean)[]` then `fns.every((fn) => fn(1))`. The element diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 496892d3..284c6e88 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -111,9 +111,7 @@ field_flow_type(f, prov, t) :- field_initializer("client", e, f), // ── the receiver takes what its field holds ───────────────────────────────── // `this.repo.find()`: the property access is typed by the field's flowed type as well // as its declared one. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_flow_type(f, prov, t). @@ -123,9 +121,7 @@ expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // fixpoint, whereas an arbitrary qualifier's type comes from expr_type and feeding that // into a negated relation would put the negation inside its own cycle. Java's // type-flow.dl draws the identical line and records the identical reason. -expr_exact_type(e, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_exact_type(e, t) :- property_access_recv(e, n, q), expr_kind("client", "THIS_REFERENCE", _, q), expr_enclosing_type(q, encl), field_in_enclosing(encl, n, f), @@ -261,6 +257,106 @@ handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), exp value_branch(a, x), method_value(x, m). +// …AND A PROXY HANDLER'S TRAPS. `new Proxy(target, handler)` and `Proxy.revocable(target, handler)` hand the runtime +// an object whose `get` / `set` / `has` … the runtime calls on every touch of the proxy. No call site names a trap, so +// what the traps call was reached by nothing: a change to the draft logic of a Proxy-based library selected none of the +// tests that create drafts. The creating site hands each trap the handler defines, the way `xs.map(cb)` hands `cb`. +// The handler is followed through the holders it is read from (a const, a reassigned `let`, a parameter it was passed +// as: holder_value), to an object literal's methods or the methods of the class a `new C()` builds, its bases included. +proxy_site(ce) :- call_site("client", "CONSTRUCTOR_CALL", "Proxy", _, _, ce, _), !type_decl("client", "Proxy", _, _, _, _, _). +proxy_site(ce) :- call_site("client", _, "revocable", "IDENTIFIER", recv, ce, _), expr_name("client", "Proxy", recv), + !type_decl("client", "Proxy", _, _, _, _, _). +proxy_handler_value(ce, a) :- proxy_site(ce), expr_child("client", ce, "ARGUMENT", "1", a). +proxy_handler_value(ce, v) :- proxy_handler_value(ce, e), expr_referenced("client", k, h, e), holder_ref_kind(k), + holder_value(h, v). +proxy_handler_value(ce, x) :- proxy_handler_value(ce, e), value_branch_of(e, x). +handed_function(ce, m) :- proxy_handler_value(ce, o), objlit_member_fn(o, name, m), proxy_trap_name(name). +handed_function(ce, m) :- proxy_handler_value(ce, n), expr_kind("client", "NEW_EXPRESSION", _, n), expr_type(n, _, t), + proxy_handler_class(t, c), method_owner("client", c, m), method_decl("client", name, _, _, m), proxy_trap_name(name). +proxy_handler_class(t, t) :- type_decl("client", _, _, _, _, _, t). +proxy_handler_class(t, a) :- type_decl("client", _, _, _, _, _, t), type_ancestor(t, a). +// a conditional or a cast between the holder and the site: `isArray ? arrayTraps : objectTraps`, `h as ProxyHandler` +value_branch_of(e, x) :- expr_kind("client", "TERNARY_EXPRESSION", _, e), expr_child("client", e, r, _, x), ternary_value_role(r). +value_branch_of(e, x) :- expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, _, _, x). +ternary_value_role("TERNARY_THEN"). +ternary_value_role("TERNARY_ELSE"). +proxy_trap_name("get"). +proxy_trap_name("set"). +proxy_trap_name("has"). +proxy_trap_name("deleteProperty"). +proxy_trap_name("ownKeys"). +proxy_trap_name("getOwnPropertyDescriptor"). +proxy_trap_name("defineProperty"). +proxy_trap_name("getPrototypeOf"). +proxy_trap_name("setPrototypeOf"). +proxy_trap_name("isExtensible"). +proxy_trap_name("preventExtensions"). +proxy_trap_name("apply"). +proxy_trap_name("construct"). + +// …AND A FUNCTION WRITTEN AS A JSX ATTRIBUTE OR CHILD. ``, +// ` + + + toggle + template handler + + + + + + + + + + diff --git a/parser/src/test-data/web/fixture/site/js/app.js b/parser/src/test-data/web/fixture/site/js/app.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/js/vendor.js b/parser/src/test-data/web/fixture/site/js/vendor.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/page.html b/parser/src/test-data/web/fixture/site/page.html new file mode 100644 index 00000000..9595250c --- /dev/null +++ b/parser/src/test-data/web/fixture/site/page.html @@ -0,0 +1,18 @@ + + +Second + + + + + + +

Second

+ + +self +

A page with an unclosed tag and a stray end tag.

+
+
implied tbody
+ + diff --git a/parser/src/test-data/web/fixture/site/partials/frame.html b/parser/src/test-data/web/fixture/site/partials/frame.html new file mode 100644 index 00000000..323136fb --- /dev/null +++ b/parser/src/test-data/web/fixture/site/partials/frame.html @@ -0,0 +1,6 @@ +
+

{% block title %}Partial{% endblock %}

+ Home + bad style + +
diff --git a/parser/src/test-data/web/fixture/site/static/css/app.css b/parser/src/test-data/web/fixture/site/static/css/app.css new file mode 100644 index 00000000..dd2d582c --- /dev/null +++ b/parser/src/test-data/web/fixture/site/static/css/app.css @@ -0,0 +1,20 @@ +@charset "utf-8"; +/* theme tokens */ +@import url("base.css") layer(base); +@import "missing.css"; +@layer base, components; +:root { --gap: 8px; --brand: #09f; } +.nav > li.item:not(.hidden) a[href^="http" i]:hover::after { color: var(--brand, red); gap: var(--gap) } +#main .card, .card--wide, ul li:nth-child(2n of .item) { background: url(../img/bg.png) no-repeat; animation: spin 1s linear infinite; font-family: "Inter", Segoe UI, sans-serif } +@media (min-width: 40em) { .card { padding: calc(var(--gap) * 2) !important } } +@keyframes spin { from { transform: rotate(0) } 50% { opacity: .5 } to { transform: rotate(1turn) } } +@font-face { font-family: "Inter"; src: url(/fonts/inter.woff2) format("woff2") } +@supports (display: grid) { .grid { display: grid } } +@container sidebar (min-width: 400px) { .card { gap: 1rem } } +.sidebar { container-name: sidebar; container-type: inline-size } +.a { color: blue; .b & { color: red } &:hover { color: green } } +.broken { color: ; } +.title::before, .title:before { content: "" } +* { box-sizing: border-box } +h1 + p ~ em { margin: 0 } +col || td { margin: 0 } diff --git a/parser/src/test-data/web/fixture/site/static/css/base.css b/parser/src/test-data/web/fixture/site/static/css/base.css new file mode 100644 index 00000000..7493ecf6 --- /dev/null +++ b/parser/src/test-data/web/fixture/site/static/css/base.css @@ -0,0 +1 @@ +body { margin: 0; -webkit-font-smoothing: antialiased } diff --git a/parser/src/test-data/web/fixture/site/static/css/empty.css b/parser/src/test-data/web/fixture/site/static/css/empty.css new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/static/css/vendor.min.css b/parser/src/test-data/web/fixture/site/static/css/vendor.min.css new file mode 100644 index 00000000..d030db28 --- /dev/null +++ b/parser/src/test-data/web/fixture/site/static/css/vendor.min.css @@ -0,0 +1 @@ +.a{color:red}.b{color:blue}.c{margin:0;padding:0} diff --git a/parser/src/test-data/web/fixture/site/static/img/hero.png b/parser/src/test-data/web/fixture/site/static/img/hero.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/static/img/logo.png b/parser/src/test-data/web/fixture/site/static/img/logo.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/static/img/sprite.svg b/parser/src/test-data/web/fixture/site/static/img/sprite.svg new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/fixture/site/vendor/sassy.css b/parser/src/test-data/web/fixture/site/vendor/sassy.css new file mode 100644 index 00000000..14a04a99 --- /dev/null +++ b/parser/src/test-data/web/fixture/site/vendor/sassy.css @@ -0,0 +1,3 @@ +$primary: #333; +@mixin flex { display: flex; } +.x { color: $primary; @include flex; } diff --git a/parser/src/test-data/web/torture/css/alt.css b/parser/src/test-data/web/torture/css/alt.css new file mode 100644 index 00000000..97121e94 --- /dev/null +++ b/parser/src/test-data/web/torture/css/alt.css @@ -0,0 +1 @@ +.alt-only { color: red } diff --git a/parser/src/test-data/web/torture/css/app.css b/parser/src/test-data/web/torture/css/app.css new file mode 100644 index 00000000..04e4d5d5 --- /dev/null +++ b/parser/src/test-data/web/torture/css/app.css @@ -0,0 +1,237 @@ +@charset "utf-8"; +/* C01 import chain: app -> tokens -> theme; layered, conditional, missing, absolute, in a layer and with media */ +@import url("tokens.css") layer(tokens); +@import "layers.css"; +@import url(nested.css) supports(selector(&)) screen and (min-width: 1px); +@import "missing.css"; +@import url("https://fonts.example.com/inter.css"); +@import "strings.css" layer(vendor.strings); +@import "fonts.css"; +@import 'selectors.css'; +@import url( "gaps.css" ); +@import "app.css"; /* self import: a cycle for the recursive join */ + +@layer tokens, base, components, utilities; +@namespace svg url(http://www.w3.org/2000/svg); +@namespace url(http://www.w3.org/1999/xhtml); + +/* C02 the class joins the HTML fixtures are built around */ +.card { color: var(--brand); } +.card--wide { max-width: var(--wide, 60rem); } +.is-active { outline: 1px solid var(--brand, var(--fallback-brand, red)); } +.nav > a[href^="#"] { color: var(--anchor); } +.upper { color: red } /* HTML has Upper: no join by case */ +.SHOUT { color: red } /* HTML has SHOUT via CLASS="SHOUT": joins */ +#loud { color: red } /* HTML has ID="LOUD": no join by case */ +#LOUD { color: red } +#padded-id { color: red } /* HTML id=" padded-id ": trimmed on the element row */ +.dup { color: red } +.a\&b { color: red } /* HTML class a&b decodes to a&b */ +.tpl-class, .hbs-class, .ko-class, .srcdoc-class { color: red } /* classes written only inside script/srcdoc text */ +.in-template, .in-template-style { color: red } +.noscript-class, .ie-only, .commented-out, .in-jinja-comment { color: red } +.js-ready, .open { color: red } /* classes only a script adds */ +.never-used-anywhere { color: red } /* dead CSS */ +.div-in-p, .li-one, .li-two, .cell, .after-self-closing-div, .unclosed-at-eof { color: red } +.custom, .customized-builtin, .hidden-attr, .dup-attr { color: red } +.static-class, .item, .wrapper, .ng-static, .vue-static, .alpine-static, .htmx-static { color: red } +.xhtml-class, .cdata-class, .after-xml-self-closing { color: red } +.in-foreign, .in-annotation, .math-class, .nested-rect, .svg-rect, .use-class, .symbol-class { color: red } +.popover, .light-card, .shadow-only, .closed-only, .slotted-item, .default-slotted, .inert-class, .deep { color: red } + +/* C03 structural selectors: the HTML writes no tbody, no html, no head, no body */ +table > tbody > tr > td.cell { color: red } +table tr td.cell { color: blue } +html > body > div.card { color: red } +body > .card { color: red } +ul > li.li-two { color: red } +p > div.div-in-p { color: red } +div > span.after-self-closing-div { color: red } +a a { color: red } + +/* C04 functional pseudo-classes: parts at depth > 0 */ +:is(.card, .nav) .item { color: red } +:where(.card) .item { color: red } +.card:not(.is-active, #section-one) { color: red } +.card:has(> .item:hover, + .nav) { color: red } +li:nth-child(2n + 1 of .li-one, .li-two) { color: red } +:nth-last-child(-n+2) { color: red } +:not(:is(.a, .b)) { color: red } +.card:is(:hover, :focus-visible):not([disabled]) { color: red } +a:not([href]) { color: red } +:host(.featured) { color: red } +:host-context(.dark) .card { color: red } +::slotted(.slotted-item) { color: red } +my-card::part(title) { color: red } +input::placeholder, ::selection, ::backdrop, ::marker, ::file-selector-button, ::highlight(h), ::view-transition-group(root), ::cue(v[voice="a"]) { color: red } +:lang(en), :dir(rtl), :state(checked), :focus-within, :target, :empty, :root, :scope, :defined, :popover-open, :modal, :user-invalid, :placeholder-shown, :indeterminate, :read-write, :autofill, :fullscreen, :picture-in-picture, :playing, :paused, :link, :any-link, :visited, :local-link, :target-within, :current, :past, :future, :active-view-transition { color: red } + +/* C05 attribute selectors: every matcher, flags, quoted/unquoted, namespaced */ +[data-page] { color: red } +[data-page="index"] { color: red } +[data-page=index] { color: red } +[class~="card"] { color: red } +[class*="card"] { color: red } +[class^=card] { color: red } +[class$="wide" i] { color: red } +[class$="wide" s] { color: red } +[id|="section"] { color: red } +[href$=".pdf" i] { color: red } +[xlink|href] { color: red } +[*|href] { color: red } +[|href] { color: red } +a[target][href][rel] { color: red } +[aria-labelledby~="section-one"] { color: red } +[style*="var("] { color: red } +input[type="IMAGE" i] { color: red } + +/* C06 combinators and universal/namespace selectors */ +* { box-sizing: border-box } +*|* { color: red } +svg|* { color: red } +|rect { color: red } +svg|rect.svg-rect { color: red } +h2 + p ~ em > b { color: red } +.a>.b+.c~.d { color: red } +col || td { color: red } +.a .b .c +.d { color: red } +.card/* comment in a selector */.is-active { color: red } +.card, /* comment between selectors */ .nav, { color: red } +.card,, .nav { color: red } +.card . broken { color: red } +.123 { color: red } +.-valid-dash { color: red } +.--double-dash { color: red } +._underscore { color: red } +#1digit { color: red } +.a.b.c.d.e.f.g.h.i.j.k { color: red } +.a#b.c#d { color: red } +div#b.c[x]:hover::before { color: red } +DIV.Card:HOVER::BEFORE { color: red } +.before:before, .after::after, ::before, :after, ::first-line, ::first-letter, :first-letter { color: red } + +/* C07 the cascade torture: same element, same property, many sources */ +#section-one { color: rgb(1, 1, 1) } /* (1,0,0) */ +.card.card--wide.is-active { color: rgb(2, 2, 2) } /* (0,3,0) */ +section.card { color: rgb(3, 3, 3) !important } /* important beats everything unlayered */ +@layer base { .card { color: rgb(4, 4, 4) } section#section-one { color: rgb(5, 5, 5) !important } } +@layer utilities { .card { color: rgb(6, 6, 6) } } +@layer { .card { color: rgb(7, 7, 7) } } /* anonymous layer */ +.card { color: rgb(8, 8, 8) } +.card { color: rgb(9, 9, 9) } /* later in source order wins over the one above */ +:where(.card.card--wide.is-active#section-one) { color: rgb(10, 10, 10) } /* (0,0,0) */ +:is(#section-one, .card) { color: rgb(11, 11, 11) } /* (1,0,0) via :is */ +.card:not(#nope) { color: rgb(12, 12, 12) } /* (1,1,0) via :not */ +[id="section-one"] { color: rgb(13, 13, 13) } /* (0,1,0) attribute, not id */ +@media screen { .card { color: rgb(14, 14, 14) } } +@media print { .card { color: rgb(15, 15, 15) } } +@supports (color: red) { .card { color: rgb(16, 16, 16) } } +@supports not (display: grid) { .card { color: rgb(17, 17, 17) } } +@container sidebar (min-width: 1px) { .card { color: rgb(18, 18, 18) } } +@scope (.card) to (.item) { :scope { color: rgb(19, 19, 19) } .is-active { color: rgb(20, 20, 20) } } +@media (400px <= width <= 700px) { .card { color: rgb(21, 21, 21) } } +@media only screen and (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .card { color: rgb(22, 22, 22) } } +@media (prefers-color-scheme: dark) { :root { --brand: black } .card { color: var(--brand) } } +@starting-style { .card { opacity: 0 } } +@page :first { margin: 1in } +@page { @top-center { content: "x" } } +@font-feature-values Inter { @styleset { nice-style: 12 } } +@counter-style thumbs { system: cyclic; symbols: "👍"; suffix: " " } +@position-try --fallback { top: anchor(bottom) } +@view-transition { navigation: auto } +@-moz-document url-prefix() { .card { color: red } } +@-webkit-keyframes vendor-spin { from { opacity: 0 } to { opacity: 1 } } +@media screen { @supports (display: grid) { @layer components { .card .item { color: rgb(23, 23, 23) } } } } + +/* C08 properties: custom, vendor, hacks, important variants, duplicates, uppercase, empty */ +.props { + --Brand: red; /* custom properties are case-sensitive: --Brand is not --brand */ + --brand: blue; + --empty:; + --with-brace: { a: b }; + --json: [1, 2, 3]; + --url: url(img/bg.png); + --nested: var(--brand, var(--Brand, var(--missing, red))); + color: var(--Brand); + background: var(--url); + -webkit-transition: all 1s; + -moz-transition: all 1s; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#80000000'); + *zoom: 1; + _height: 1px; + color: red\9; + width: expression(document.body.clientWidth > 800 ? "800px" : "auto"); + margin: 0 ! important; + padding: 0!important; + color: red !IMPORTANT; + color: red;; + color: ; + : red; + color red; + COLOR: RED; + Color: Red; + display: grid; display: -ms-grid; + grid-template-areas: "a b" "c d"; + content: "a; } b { c: d }"; /* string with declaration-ending characters */ + content: "var(--not-a-var) url(not-a-url.png)"; /* string that looks like functions */ + background: url( "img/bg.png" ); + background: url('img/bg.png?v=1#frag'); + background: url(img/bg.png?v=1#frag); + background: url( img/bg.png ); + background: url("img/space name.png"); + background: url(img/space\ name.png); + background: url(data:image/png;base64,iVBORw0KGgo=); + background: url(//cdn.example.com/x.png); + background: url(/img/bg.png); + background: url(../img/bg.png); + background: url(#inline-ref); + background: url(); + background: url(""), url(img/bg.png), url(img/missing.png); + background: image-set("img/hero.png" 1x, "img/hero@2x.png" 2x); + background: -webkit-image-set(url(img/hero.png) 1x); + background: src("img/bg.png"); + mask: url(img/sprite.svg#mask); + cursor: url(img/cursor.cur) 2 2, url(img/cursor.cur), pointer; + list-style: url(img/bg.png); + border-image: url(img/bg.png) 30 round; + background: URL(img/bg.png); + color: color-mix(in srgb, var(--brand) 50%, var(--Brand)); + width: calc(100% - var(--gap, 8px) * 2); + padding: env(safe-area-inset-top, var(--pad)); + font: italic bold 12px/30px Inter, "Segoe UI", serif; /* font SHORTHAND: family names at the end */ + font: 12px system-ui; + font: menu; + font-family: Inter; + font-family: "Inter", 'Roboto Mono', Segoe UI, ui-sans-serif, sans-serif; + font-family: inherit; + font-family: var(--font); + animation: spin 1s linear infinite, fade 2s; + animation: 1s ease-in-out infinite reverse spin; + animation: 2s steps(4, end) slide; + animation: "quoted-name" 1s; + animation: none; + animation: linear 1s; /* linear is a keyword, not a name */ + animation: var(--anim) 1s; + animation-name: spin, fade, missing-keyframes, vendor-spin; + animation-name: none; + animation-name: ease; /* a keyframes named like a keyword */ + container: sidebar / inline-size; + container: a b / size; + container-name: sidebar other; + container-name: none; + container-type: inline-size; + transition: color var(--duration) var(--easing, ease); + will-change: transform; + content: counter(item) ". "; + grid-area: header; + position-try-fallbacks: --fallback; + anchor-name: --anchor; + position-anchor: --anchor; + view-transition-name: hero; + offset-path: url(#motion-path); + clip-path: url(img/sprite.svg#clip); + shape-outside: url(img/bg.png); + src: url(fonts/inter.woff2); /* src outside @font-face */ +} diff --git a/parser/src/test-data/web/torture/css/base.css b/parser/src/test-data/web/torture/css/base.css new file mode 100644 index 00000000..9a0e91a3 --- /dev/null +++ b/parser/src/test-data/web/torture/css/base.css @@ -0,0 +1,2 @@ +.base-class { color: red } +.card { color: rgb(100,100,100) } diff --git a/parser/src/test-data/web/torture/css/big.css b/parser/src/test-data/web/torture/css/big.css new file mode 100644 index 00000000..217950d0 --- /dev/null +++ b/parser/src/test-data/web/torture/css/big.css @@ -0,0 +1,1500 @@ +.big-0 { color: var(--brand); background: url(img/bg.png) } +.big-1 { color: var(--brand); background: url(img/bg.png) } +.big-2 { color: var(--brand); background: url(img/bg.png) } +.big-3 { color: var(--brand); background: url(img/bg.png) } +.big-4 { color: var(--brand); background: url(img/bg.png) } +.big-5 { color: var(--brand); background: url(img/bg.png) } +.big-6 { color: var(--brand); background: url(img/bg.png) } +.big-7 { color: var(--brand); background: url(img/bg.png) } +.big-8 { color: var(--brand); background: url(img/bg.png) } +.big-9 { color: var(--brand); background: url(img/bg.png) } +.big-10 { color: var(--brand); background: url(img/bg.png) } +.big-11 { color: var(--brand); background: url(img/bg.png) } +.big-12 { color: var(--brand); background: url(img/bg.png) } +.big-13 { color: var(--brand); background: url(img/bg.png) } +.big-14 { color: var(--brand); background: url(img/bg.png) } +.big-15 { color: var(--brand); background: url(img/bg.png) } +.big-16 { color: var(--brand); background: url(img/bg.png) } +.big-17 { color: var(--brand); background: url(img/bg.png) } +.big-18 { color: var(--brand); background: url(img/bg.png) } +.big-19 { color: var(--brand); background: url(img/bg.png) } +.big-20 { color: var(--brand); background: url(img/bg.png) } +.big-21 { color: var(--brand); background: url(img/bg.png) } +.big-22 { color: var(--brand); background: url(img/bg.png) } +.big-23 { color: var(--brand); background: url(img/bg.png) } +.big-24 { color: var(--brand); background: url(img/bg.png) } +.big-25 { color: var(--brand); background: url(img/bg.png) } +.big-26 { color: var(--brand); background: url(img/bg.png) } +.big-27 { color: var(--brand); background: url(img/bg.png) } +.big-28 { color: var(--brand); background: url(img/bg.png) } +.big-29 { color: var(--brand); background: url(img/bg.png) } +.big-30 { color: var(--brand); background: url(img/bg.png) } +.big-31 { color: var(--brand); background: url(img/bg.png) } +.big-32 { color: var(--brand); background: url(img/bg.png) } +.big-33 { color: var(--brand); background: url(img/bg.png) } +.big-34 { color: var(--brand); background: url(img/bg.png) } +.big-35 { color: var(--brand); background: url(img/bg.png) } +.big-36 { color: var(--brand); background: url(img/bg.png) } +.big-37 { color: var(--brand); background: url(img/bg.png) } +.big-38 { color: var(--brand); background: url(img/bg.png) } +.big-39 { color: var(--brand); background: url(img/bg.png) } +.big-40 { color: var(--brand); background: url(img/bg.png) } +.big-41 { color: var(--brand); background: url(img/bg.png) } +.big-42 { color: var(--brand); background: url(img/bg.png) } +.big-43 { color: var(--brand); background: url(img/bg.png) } +.big-44 { color: var(--brand); background: url(img/bg.png) } +.big-45 { color: var(--brand); background: url(img/bg.png) } +.big-46 { color: var(--brand); background: url(img/bg.png) } +.big-47 { color: var(--brand); background: url(img/bg.png) } +.big-48 { color: var(--brand); background: url(img/bg.png) } +.big-49 { color: var(--brand); background: url(img/bg.png) } +.big-50 { color: var(--brand); background: url(img/bg.png) } +.big-51 { color: var(--brand); background: url(img/bg.png) } +.big-52 { color: var(--brand); background: url(img/bg.png) } +.big-53 { color: var(--brand); background: url(img/bg.png) } +.big-54 { color: var(--brand); background: url(img/bg.png) } +.big-55 { color: var(--brand); background: url(img/bg.png) } +.big-56 { color: var(--brand); background: url(img/bg.png) } +.big-57 { color: var(--brand); background: url(img/bg.png) } +.big-58 { color: var(--brand); background: url(img/bg.png) } +.big-59 { color: var(--brand); background: url(img/bg.png) } +.big-60 { color: var(--brand); background: url(img/bg.png) } +.big-61 { color: var(--brand); background: url(img/bg.png) } +.big-62 { color: var(--brand); background: url(img/bg.png) } +.big-63 { color: var(--brand); background: url(img/bg.png) } +.big-64 { color: var(--brand); background: url(img/bg.png) } +.big-65 { color: var(--brand); background: url(img/bg.png) } +.big-66 { color: var(--brand); background: url(img/bg.png) } +.big-67 { color: var(--brand); background: url(img/bg.png) } +.big-68 { color: var(--brand); background: url(img/bg.png) } +.big-69 { color: var(--brand); background: url(img/bg.png) } +.big-70 { color: var(--brand); background: url(img/bg.png) } +.big-71 { color: var(--brand); background: url(img/bg.png) } +.big-72 { color: var(--brand); background: url(img/bg.png) } +.big-73 { color: var(--brand); background: url(img/bg.png) } +.big-74 { color: var(--brand); background: url(img/bg.png) } +.big-75 { color: var(--brand); background: url(img/bg.png) } +.big-76 { color: var(--brand); background: url(img/bg.png) } +.big-77 { color: var(--brand); background: url(img/bg.png) } +.big-78 { color: var(--brand); background: url(img/bg.png) } +.big-79 { color: var(--brand); background: url(img/bg.png) } +.big-80 { color: var(--brand); background: url(img/bg.png) } +.big-81 { color: var(--brand); background: url(img/bg.png) } +.big-82 { color: var(--brand); background: url(img/bg.png) } +.big-83 { color: var(--brand); background: url(img/bg.png) } +.big-84 { color: var(--brand); background: url(img/bg.png) } +.big-85 { color: var(--brand); background: url(img/bg.png) } +.big-86 { color: var(--brand); background: url(img/bg.png) } +.big-87 { color: var(--brand); background: url(img/bg.png) } +.big-88 { color: var(--brand); background: url(img/bg.png) } +.big-89 { color: var(--brand); background: url(img/bg.png) } +.big-90 { color: var(--brand); background: url(img/bg.png) } +.big-91 { color: var(--brand); background: url(img/bg.png) } +.big-92 { color: var(--brand); background: url(img/bg.png) } +.big-93 { color: var(--brand); background: url(img/bg.png) } +.big-94 { color: var(--brand); background: url(img/bg.png) } +.big-95 { color: var(--brand); background: url(img/bg.png) } +.big-96 { color: var(--brand); background: url(img/bg.png) } +.big-97 { color: var(--brand); background: url(img/bg.png) } +.big-98 { color: var(--brand); background: url(img/bg.png) } +.big-99 { color: var(--brand); background: url(img/bg.png) } +.big-100 { color: var(--brand); background: url(img/bg.png) } +.big-101 { color: var(--brand); background: url(img/bg.png) } +.big-102 { color: var(--brand); background: url(img/bg.png) } +.big-103 { color: var(--brand); background: url(img/bg.png) } +.big-104 { color: var(--brand); background: url(img/bg.png) } +.big-105 { color: var(--brand); background: url(img/bg.png) } +.big-106 { color: var(--brand); background: url(img/bg.png) } +.big-107 { color: var(--brand); background: url(img/bg.png) } +.big-108 { color: var(--brand); background: url(img/bg.png) } +.big-109 { color: var(--brand); background: url(img/bg.png) } +.big-110 { color: var(--brand); background: url(img/bg.png) } +.big-111 { color: var(--brand); background: url(img/bg.png) } +.big-112 { color: var(--brand); background: url(img/bg.png) } +.big-113 { color: var(--brand); background: url(img/bg.png) } +.big-114 { color: var(--brand); background: url(img/bg.png) } +.big-115 { color: var(--brand); background: url(img/bg.png) } +.big-116 { color: var(--brand); background: url(img/bg.png) } +.big-117 { color: var(--brand); background: url(img/bg.png) } +.big-118 { color: var(--brand); background: url(img/bg.png) } +.big-119 { color: var(--brand); background: url(img/bg.png) } +.big-120 { color: var(--brand); background: url(img/bg.png) } +.big-121 { color: var(--brand); background: url(img/bg.png) } +.big-122 { color: var(--brand); background: url(img/bg.png) } +.big-123 { color: var(--brand); background: url(img/bg.png) } +.big-124 { color: var(--brand); background: url(img/bg.png) } +.big-125 { color: var(--brand); background: url(img/bg.png) } +.big-126 { color: var(--brand); background: url(img/bg.png) } +.big-127 { color: var(--brand); background: url(img/bg.png) } +.big-128 { color: var(--brand); background: url(img/bg.png) } +.big-129 { color: var(--brand); background: url(img/bg.png) } +.big-130 { color: var(--brand); background: url(img/bg.png) } +.big-131 { color: var(--brand); background: url(img/bg.png) } +.big-132 { color: var(--brand); background: url(img/bg.png) } +.big-133 { color: var(--brand); background: url(img/bg.png) } +.big-134 { color: var(--brand); background: url(img/bg.png) } +.big-135 { color: var(--brand); background: url(img/bg.png) } +.big-136 { color: var(--brand); background: url(img/bg.png) } +.big-137 { color: var(--brand); background: url(img/bg.png) } +.big-138 { color: var(--brand); background: url(img/bg.png) } +.big-139 { color: var(--brand); background: url(img/bg.png) } +.big-140 { color: var(--brand); background: url(img/bg.png) } +.big-141 { color: var(--brand); background: url(img/bg.png) } +.big-142 { color: var(--brand); background: url(img/bg.png) } +.big-143 { color: var(--brand); background: url(img/bg.png) } +.big-144 { color: var(--brand); background: url(img/bg.png) } +.big-145 { color: var(--brand); background: url(img/bg.png) } +.big-146 { color: var(--brand); background: url(img/bg.png) } +.big-147 { color: var(--brand); background: url(img/bg.png) } +.big-148 { color: var(--brand); background: url(img/bg.png) } +.big-149 { color: var(--brand); background: url(img/bg.png) } +.big-150 { color: var(--brand); background: url(img/bg.png) } +.big-151 { color: var(--brand); background: url(img/bg.png) } +.big-152 { color: var(--brand); background: url(img/bg.png) } +.big-153 { color: var(--brand); background: url(img/bg.png) } +.big-154 { color: var(--brand); background: url(img/bg.png) } +.big-155 { color: var(--brand); background: url(img/bg.png) } +.big-156 { color: var(--brand); background: url(img/bg.png) } +.big-157 { color: var(--brand); background: url(img/bg.png) } +.big-158 { color: var(--brand); background: url(img/bg.png) } +.big-159 { color: var(--brand); background: url(img/bg.png) } +.big-160 { color: var(--brand); background: url(img/bg.png) } +.big-161 { color: var(--brand); background: url(img/bg.png) } +.big-162 { color: var(--brand); background: url(img/bg.png) } +.big-163 { color: var(--brand); background: url(img/bg.png) } +.big-164 { color: var(--brand); background: url(img/bg.png) } +.big-165 { color: var(--brand); background: url(img/bg.png) } +.big-166 { color: var(--brand); background: url(img/bg.png) } +.big-167 { color: var(--brand); background: url(img/bg.png) } +.big-168 { color: var(--brand); background: url(img/bg.png) } +.big-169 { color: var(--brand); background: url(img/bg.png) } +.big-170 { color: var(--brand); background: url(img/bg.png) } +.big-171 { color: var(--brand); background: url(img/bg.png) } +.big-172 { color: var(--brand); background: url(img/bg.png) } +.big-173 { color: var(--brand); background: url(img/bg.png) } +.big-174 { color: var(--brand); background: url(img/bg.png) } +.big-175 { color: var(--brand); background: url(img/bg.png) } +.big-176 { color: var(--brand); background: url(img/bg.png) } +.big-177 { color: var(--brand); background: url(img/bg.png) } +.big-178 { color: var(--brand); background: url(img/bg.png) } +.big-179 { color: var(--brand); background: url(img/bg.png) } +.big-180 { color: var(--brand); background: url(img/bg.png) } +.big-181 { color: var(--brand); background: url(img/bg.png) } +.big-182 { color: var(--brand); background: url(img/bg.png) } +.big-183 { color: var(--brand); background: url(img/bg.png) } +.big-184 { color: var(--brand); background: url(img/bg.png) } +.big-185 { color: var(--brand); background: url(img/bg.png) } +.big-186 { color: var(--brand); background: url(img/bg.png) } +.big-187 { color: var(--brand); background: url(img/bg.png) } +.big-188 { color: var(--brand); background: url(img/bg.png) } +.big-189 { color: var(--brand); background: url(img/bg.png) } +.big-190 { color: var(--brand); background: url(img/bg.png) } +.big-191 { color: var(--brand); background: url(img/bg.png) } +.big-192 { color: var(--brand); background: url(img/bg.png) } +.big-193 { color: var(--brand); background: url(img/bg.png) } +.big-194 { color: var(--brand); background: url(img/bg.png) } +.big-195 { color: var(--brand); background: url(img/bg.png) } +.big-196 { color: var(--brand); background: url(img/bg.png) } +.big-197 { color: var(--brand); background: url(img/bg.png) } +.big-198 { color: var(--brand); background: url(img/bg.png) } +.big-199 { color: var(--brand); background: url(img/bg.png) } +.big-200 { color: var(--brand); background: url(img/bg.png) } +.big-201 { color: var(--brand); background: url(img/bg.png) } +.big-202 { color: var(--brand); background: url(img/bg.png) } +.big-203 { color: var(--brand); background: url(img/bg.png) } +.big-204 { color: var(--brand); background: url(img/bg.png) } +.big-205 { color: var(--brand); background: url(img/bg.png) } +.big-206 { color: var(--brand); background: url(img/bg.png) } +.big-207 { color: var(--brand); background: url(img/bg.png) } +.big-208 { color: var(--brand); background: url(img/bg.png) } +.big-209 { color: var(--brand); background: url(img/bg.png) } +.big-210 { color: var(--brand); background: url(img/bg.png) } +.big-211 { color: var(--brand); background: url(img/bg.png) } +.big-212 { color: var(--brand); background: url(img/bg.png) } +.big-213 { color: var(--brand); background: url(img/bg.png) } +.big-214 { color: var(--brand); background: url(img/bg.png) } +.big-215 { color: var(--brand); background: url(img/bg.png) } +.big-216 { color: var(--brand); background: url(img/bg.png) } +.big-217 { color: var(--brand); background: url(img/bg.png) } +.big-218 { color: var(--brand); background: url(img/bg.png) } +.big-219 { color: var(--brand); background: url(img/bg.png) } +.big-220 { color: var(--brand); background: url(img/bg.png) } +.big-221 { color: var(--brand); background: url(img/bg.png) } +.big-222 { color: var(--brand); background: url(img/bg.png) } +.big-223 { color: var(--brand); background: url(img/bg.png) } +.big-224 { color: var(--brand); background: url(img/bg.png) } +.big-225 { color: var(--brand); background: url(img/bg.png) } +.big-226 { color: var(--brand); background: url(img/bg.png) } +.big-227 { color: var(--brand); background: url(img/bg.png) } +.big-228 { color: var(--brand); background: url(img/bg.png) } +.big-229 { color: var(--brand); background: url(img/bg.png) } +.big-230 { color: var(--brand); background: url(img/bg.png) } +.big-231 { color: var(--brand); background: url(img/bg.png) } +.big-232 { color: var(--brand); background: url(img/bg.png) } +.big-233 { color: var(--brand); background: url(img/bg.png) } +.big-234 { color: var(--brand); background: url(img/bg.png) } +.big-235 { color: var(--brand); background: url(img/bg.png) } +.big-236 { color: var(--brand); background: url(img/bg.png) } +.big-237 { color: var(--brand); background: url(img/bg.png) } +.big-238 { color: var(--brand); background: url(img/bg.png) } +.big-239 { color: var(--brand); background: url(img/bg.png) } +.big-240 { color: var(--brand); background: url(img/bg.png) } +.big-241 { color: var(--brand); background: url(img/bg.png) } +.big-242 { color: var(--brand); background: url(img/bg.png) } +.big-243 { color: var(--brand); background: url(img/bg.png) } +.big-244 { color: var(--brand); background: url(img/bg.png) } +.big-245 { color: var(--brand); background: url(img/bg.png) } +.big-246 { color: var(--brand); background: url(img/bg.png) } +.big-247 { color: var(--brand); background: url(img/bg.png) } +.big-248 { color: var(--brand); background: url(img/bg.png) } +.big-249 { color: var(--brand); background: url(img/bg.png) } +.big-250 { color: var(--brand); background: url(img/bg.png) } +.big-251 { color: var(--brand); background: url(img/bg.png) } +.big-252 { color: var(--brand); background: url(img/bg.png) } +.big-253 { color: var(--brand); background: url(img/bg.png) } +.big-254 { color: var(--brand); background: url(img/bg.png) } +.big-255 { color: var(--brand); background: url(img/bg.png) } +.big-256 { color: var(--brand); background: url(img/bg.png) } +.big-257 { color: var(--brand); background: url(img/bg.png) } +.big-258 { color: var(--brand); background: url(img/bg.png) } +.big-259 { color: var(--brand); background: url(img/bg.png) } +.big-260 { color: var(--brand); background: url(img/bg.png) } +.big-261 { color: var(--brand); background: url(img/bg.png) } +.big-262 { color: var(--brand); background: url(img/bg.png) } +.big-263 { color: var(--brand); background: url(img/bg.png) } +.big-264 { color: var(--brand); background: url(img/bg.png) } +.big-265 { color: var(--brand); background: url(img/bg.png) } +.big-266 { color: var(--brand); background: url(img/bg.png) } +.big-267 { color: var(--brand); background: url(img/bg.png) } +.big-268 { color: var(--brand); background: url(img/bg.png) } +.big-269 { color: var(--brand); background: url(img/bg.png) } +.big-270 { color: var(--brand); background: url(img/bg.png) } +.big-271 { color: var(--brand); background: url(img/bg.png) } +.big-272 { color: var(--brand); background: url(img/bg.png) } +.big-273 { color: var(--brand); background: url(img/bg.png) } +.big-274 { color: var(--brand); background: url(img/bg.png) } +.big-275 { color: var(--brand); background: url(img/bg.png) } +.big-276 { color: var(--brand); background: url(img/bg.png) } +.big-277 { color: var(--brand); background: url(img/bg.png) } +.big-278 { color: var(--brand); background: url(img/bg.png) } +.big-279 { color: var(--brand); background: url(img/bg.png) } +.big-280 { color: var(--brand); background: url(img/bg.png) } +.big-281 { color: var(--brand); background: url(img/bg.png) } +.big-282 { color: var(--brand); background: url(img/bg.png) } +.big-283 { color: var(--brand); background: url(img/bg.png) } +.big-284 { color: var(--brand); background: url(img/bg.png) } +.big-285 { color: var(--brand); background: url(img/bg.png) } +.big-286 { color: var(--brand); background: url(img/bg.png) } +.big-287 { color: var(--brand); background: url(img/bg.png) } +.big-288 { color: var(--brand); background: url(img/bg.png) } +.big-289 { color: var(--brand); background: url(img/bg.png) } +.big-290 { color: var(--brand); background: url(img/bg.png) } +.big-291 { color: var(--brand); background: url(img/bg.png) } +.big-292 { color: var(--brand); background: url(img/bg.png) } +.big-293 { color: var(--brand); background: url(img/bg.png) } +.big-294 { color: var(--brand); background: url(img/bg.png) } +.big-295 { color: var(--brand); background: url(img/bg.png) } +.big-296 { color: var(--brand); background: url(img/bg.png) } +.big-297 { color: var(--brand); background: url(img/bg.png) } +.big-298 { color: var(--brand); background: url(img/bg.png) } +.big-299 { color: var(--brand); background: url(img/bg.png) } +.big-300 { color: var(--brand); background: url(img/bg.png) } +.big-301 { color: var(--brand); background: url(img/bg.png) } +.big-302 { color: var(--brand); background: url(img/bg.png) } +.big-303 { color: var(--brand); background: url(img/bg.png) } +.big-304 { color: var(--brand); background: url(img/bg.png) } +.big-305 { color: var(--brand); background: url(img/bg.png) } +.big-306 { color: var(--brand); background: url(img/bg.png) } +.big-307 { color: var(--brand); background: url(img/bg.png) } +.big-308 { color: var(--brand); background: url(img/bg.png) } +.big-309 { color: var(--brand); background: url(img/bg.png) } +.big-310 { color: var(--brand); background: url(img/bg.png) } +.big-311 { color: var(--brand); background: url(img/bg.png) } +.big-312 { color: var(--brand); background: url(img/bg.png) } +.big-313 { color: var(--brand); background: url(img/bg.png) } +.big-314 { color: var(--brand); background: url(img/bg.png) } +.big-315 { color: var(--brand); background: url(img/bg.png) } +.big-316 { color: var(--brand); background: url(img/bg.png) } +.big-317 { color: var(--brand); background: url(img/bg.png) } +.big-318 { color: var(--brand); background: url(img/bg.png) } +.big-319 { color: var(--brand); background: url(img/bg.png) } +.big-320 { color: var(--brand); background: url(img/bg.png) } +.big-321 { color: var(--brand); background: url(img/bg.png) } +.big-322 { color: var(--brand); background: url(img/bg.png) } +.big-323 { color: var(--brand); background: url(img/bg.png) } +.big-324 { color: var(--brand); background: url(img/bg.png) } +.big-325 { color: var(--brand); background: url(img/bg.png) } +.big-326 { color: var(--brand); background: url(img/bg.png) } +.big-327 { color: var(--brand); background: url(img/bg.png) } +.big-328 { color: var(--brand); background: url(img/bg.png) } +.big-329 { color: var(--brand); background: url(img/bg.png) } +.big-330 { color: var(--brand); background: url(img/bg.png) } +.big-331 { color: var(--brand); background: url(img/bg.png) } +.big-332 { color: var(--brand); background: url(img/bg.png) } +.big-333 { color: var(--brand); background: url(img/bg.png) } +.big-334 { color: var(--brand); background: url(img/bg.png) } +.big-335 { color: var(--brand); background: url(img/bg.png) } +.big-336 { color: var(--brand); background: url(img/bg.png) } +.big-337 { color: var(--brand); background: url(img/bg.png) } +.big-338 { color: var(--brand); background: url(img/bg.png) } +.big-339 { color: var(--brand); background: url(img/bg.png) } +.big-340 { color: var(--brand); background: url(img/bg.png) } +.big-341 { color: var(--brand); background: url(img/bg.png) } +.big-342 { color: var(--brand); background: url(img/bg.png) } +.big-343 { color: var(--brand); background: url(img/bg.png) } +.big-344 { color: var(--brand); background: url(img/bg.png) } +.big-345 { color: var(--brand); background: url(img/bg.png) } +.big-346 { color: var(--brand); background: url(img/bg.png) } +.big-347 { color: var(--brand); background: url(img/bg.png) } +.big-348 { color: var(--brand); background: url(img/bg.png) } +.big-349 { color: var(--brand); background: url(img/bg.png) } +.big-350 { color: var(--brand); background: url(img/bg.png) } +.big-351 { color: var(--brand); background: url(img/bg.png) } +.big-352 { color: var(--brand); background: url(img/bg.png) } +.big-353 { color: var(--brand); background: url(img/bg.png) } +.big-354 { color: var(--brand); background: url(img/bg.png) } +.big-355 { color: var(--brand); background: url(img/bg.png) } +.big-356 { color: var(--brand); background: url(img/bg.png) } +.big-357 { color: var(--brand); background: url(img/bg.png) } +.big-358 { color: var(--brand); background: url(img/bg.png) } +.big-359 { color: var(--brand); background: url(img/bg.png) } +.big-360 { color: var(--brand); background: url(img/bg.png) } +.big-361 { color: var(--brand); background: url(img/bg.png) } +.big-362 { color: var(--brand); background: url(img/bg.png) } +.big-363 { color: var(--brand); background: url(img/bg.png) } +.big-364 { color: var(--brand); background: url(img/bg.png) } +.big-365 { color: var(--brand); background: url(img/bg.png) } +.big-366 { color: var(--brand); background: url(img/bg.png) } +.big-367 { color: var(--brand); background: url(img/bg.png) } +.big-368 { color: var(--brand); background: url(img/bg.png) } +.big-369 { color: var(--brand); background: url(img/bg.png) } +.big-370 { color: var(--brand); background: url(img/bg.png) } +.big-371 { color: var(--brand); background: url(img/bg.png) } +.big-372 { color: var(--brand); background: url(img/bg.png) } +.big-373 { color: var(--brand); background: url(img/bg.png) } +.big-374 { color: var(--brand); background: url(img/bg.png) } +.big-375 { color: var(--brand); background: url(img/bg.png) } +.big-376 { color: var(--brand); background: url(img/bg.png) } +.big-377 { color: var(--brand); background: url(img/bg.png) } +.big-378 { color: var(--brand); background: url(img/bg.png) } +.big-379 { color: var(--brand); background: url(img/bg.png) } +.big-380 { color: var(--brand); background: url(img/bg.png) } +.big-381 { color: var(--brand); background: url(img/bg.png) } +.big-382 { color: var(--brand); background: url(img/bg.png) } +.big-383 { color: var(--brand); background: url(img/bg.png) } +.big-384 { color: var(--brand); background: url(img/bg.png) } +.big-385 { color: var(--brand); background: url(img/bg.png) } +.big-386 { color: var(--brand); background: url(img/bg.png) } +.big-387 { color: var(--brand); background: url(img/bg.png) } +.big-388 { color: var(--brand); background: url(img/bg.png) } +.big-389 { color: var(--brand); background: url(img/bg.png) } +.big-390 { color: var(--brand); background: url(img/bg.png) } +.big-391 { color: var(--brand); background: url(img/bg.png) } +.big-392 { color: var(--brand); background: url(img/bg.png) } +.big-393 { color: var(--brand); background: url(img/bg.png) } +.big-394 { color: var(--brand); background: url(img/bg.png) } +.big-395 { color: var(--brand); background: url(img/bg.png) } +.big-396 { color: var(--brand); background: url(img/bg.png) } +.big-397 { color: var(--brand); background: url(img/bg.png) } +.big-398 { color: var(--brand); background: url(img/bg.png) } +.big-399 { color: var(--brand); background: url(img/bg.png) } +.big-400 { color: var(--brand); background: url(img/bg.png) } +.big-401 { color: var(--brand); background: url(img/bg.png) } +.big-402 { color: var(--brand); background: url(img/bg.png) } +.big-403 { color: var(--brand); background: url(img/bg.png) } +.big-404 { color: var(--brand); background: url(img/bg.png) } +.big-405 { color: var(--brand); background: url(img/bg.png) } +.big-406 { color: var(--brand); background: url(img/bg.png) } +.big-407 { color: var(--brand); background: url(img/bg.png) } +.big-408 { color: var(--brand); background: url(img/bg.png) } +.big-409 { color: var(--brand); background: url(img/bg.png) } +.big-410 { color: var(--brand); background: url(img/bg.png) } +.big-411 { color: var(--brand); background: url(img/bg.png) } +.big-412 { color: var(--brand); background: url(img/bg.png) } +.big-413 { color: var(--brand); background: url(img/bg.png) } +.big-414 { color: var(--brand); background: url(img/bg.png) } +.big-415 { color: var(--brand); background: url(img/bg.png) } +.big-416 { color: var(--brand); background: url(img/bg.png) } +.big-417 { color: var(--brand); background: url(img/bg.png) } +.big-418 { color: var(--brand); background: url(img/bg.png) } +.big-419 { color: var(--brand); background: url(img/bg.png) } +.big-420 { color: var(--brand); background: url(img/bg.png) } +.big-421 { color: var(--brand); background: url(img/bg.png) } +.big-422 { color: var(--brand); background: url(img/bg.png) } +.big-423 { color: var(--brand); background: url(img/bg.png) } +.big-424 { color: var(--brand); background: url(img/bg.png) } +.big-425 { color: var(--brand); background: url(img/bg.png) } +.big-426 { color: var(--brand); background: url(img/bg.png) } +.big-427 { color: var(--brand); background: url(img/bg.png) } +.big-428 { color: var(--brand); background: url(img/bg.png) } +.big-429 { color: var(--brand); background: url(img/bg.png) } +.big-430 { color: var(--brand); background: url(img/bg.png) } +.big-431 { color: var(--brand); background: url(img/bg.png) } +.big-432 { color: var(--brand); background: url(img/bg.png) } +.big-433 { color: var(--brand); background: url(img/bg.png) } +.big-434 { color: var(--brand); background: url(img/bg.png) } +.big-435 { color: var(--brand); background: url(img/bg.png) } +.big-436 { color: var(--brand); background: url(img/bg.png) } +.big-437 { color: var(--brand); background: url(img/bg.png) } +.big-438 { color: var(--brand); background: url(img/bg.png) } +.big-439 { color: var(--brand); background: url(img/bg.png) } +.big-440 { color: var(--brand); background: url(img/bg.png) } +.big-441 { color: var(--brand); background: url(img/bg.png) } +.big-442 { color: var(--brand); background: url(img/bg.png) } +.big-443 { color: var(--brand); background: url(img/bg.png) } +.big-444 { color: var(--brand); background: url(img/bg.png) } +.big-445 { color: var(--brand); background: url(img/bg.png) } +.big-446 { color: var(--brand); background: url(img/bg.png) } +.big-447 { color: var(--brand); background: url(img/bg.png) } +.big-448 { color: var(--brand); background: url(img/bg.png) } +.big-449 { color: var(--brand); background: url(img/bg.png) } +.big-450 { color: var(--brand); background: url(img/bg.png) } +.big-451 { color: var(--brand); background: url(img/bg.png) } +.big-452 { color: var(--brand); background: url(img/bg.png) } +.big-453 { color: var(--brand); background: url(img/bg.png) } +.big-454 { color: var(--brand); background: url(img/bg.png) } +.big-455 { color: var(--brand); background: url(img/bg.png) } +.big-456 { color: var(--brand); background: url(img/bg.png) } +.big-457 { color: var(--brand); background: url(img/bg.png) } +.big-458 { color: var(--brand); background: url(img/bg.png) } +.big-459 { color: var(--brand); background: url(img/bg.png) } +.big-460 { color: var(--brand); background: url(img/bg.png) } +.big-461 { color: var(--brand); background: url(img/bg.png) } +.big-462 { color: var(--brand); background: url(img/bg.png) } +.big-463 { color: var(--brand); background: url(img/bg.png) } +.big-464 { color: var(--brand); background: url(img/bg.png) } +.big-465 { color: var(--brand); background: url(img/bg.png) } +.big-466 { color: var(--brand); background: url(img/bg.png) } +.big-467 { color: var(--brand); background: url(img/bg.png) } +.big-468 { color: var(--brand); background: url(img/bg.png) } +.big-469 { color: var(--brand); background: url(img/bg.png) } +.big-470 { color: var(--brand); background: url(img/bg.png) } +.big-471 { color: var(--brand); background: url(img/bg.png) } +.big-472 { color: var(--brand); background: url(img/bg.png) } +.big-473 { color: var(--brand); background: url(img/bg.png) } +.big-474 { color: var(--brand); background: url(img/bg.png) } +.big-475 { color: var(--brand); background: url(img/bg.png) } +.big-476 { color: var(--brand); background: url(img/bg.png) } +.big-477 { color: var(--brand); background: url(img/bg.png) } +.big-478 { color: var(--brand); background: url(img/bg.png) } +.big-479 { color: var(--brand); background: url(img/bg.png) } +.big-480 { color: var(--brand); background: url(img/bg.png) } +.big-481 { color: var(--brand); background: url(img/bg.png) } +.big-482 { color: var(--brand); background: url(img/bg.png) } +.big-483 { color: var(--brand); background: url(img/bg.png) } +.big-484 { color: var(--brand); background: url(img/bg.png) } +.big-485 { color: var(--brand); background: url(img/bg.png) } +.big-486 { color: var(--brand); background: url(img/bg.png) } +.big-487 { color: var(--brand); background: url(img/bg.png) } +.big-488 { color: var(--brand); background: url(img/bg.png) } +.big-489 { color: var(--brand); background: url(img/bg.png) } +.big-490 { color: var(--brand); background: url(img/bg.png) } +.big-491 { color: var(--brand); background: url(img/bg.png) } +.big-492 { color: var(--brand); background: url(img/bg.png) } +.big-493 { color: var(--brand); background: url(img/bg.png) } +.big-494 { color: var(--brand); background: url(img/bg.png) } +.big-495 { color: var(--brand); background: url(img/bg.png) } +.big-496 { color: var(--brand); background: url(img/bg.png) } +.big-497 { color: var(--brand); background: url(img/bg.png) } +.big-498 { color: var(--brand); background: url(img/bg.png) } +.big-499 { color: var(--brand); background: url(img/bg.png) } +.big-500 { color: var(--brand); background: url(img/bg.png) } +.big-501 { color: var(--brand); background: url(img/bg.png) } +.big-502 { color: var(--brand); background: url(img/bg.png) } +.big-503 { color: var(--brand); background: url(img/bg.png) } +.big-504 { color: var(--brand); background: url(img/bg.png) } +.big-505 { color: var(--brand); background: url(img/bg.png) } +.big-506 { color: var(--brand); background: url(img/bg.png) } +.big-507 { color: var(--brand); background: url(img/bg.png) } +.big-508 { color: var(--brand); background: url(img/bg.png) } +.big-509 { color: var(--brand); background: url(img/bg.png) } +.big-510 { color: var(--brand); background: url(img/bg.png) } +.big-511 { color: var(--brand); background: url(img/bg.png) } +.big-512 { color: var(--brand); background: url(img/bg.png) } +.big-513 { color: var(--brand); background: url(img/bg.png) } +.big-514 { color: var(--brand); background: url(img/bg.png) } +.big-515 { color: var(--brand); background: url(img/bg.png) } +.big-516 { color: var(--brand); background: url(img/bg.png) } +.big-517 { color: var(--brand); background: url(img/bg.png) } +.big-518 { color: var(--brand); background: url(img/bg.png) } +.big-519 { color: var(--brand); background: url(img/bg.png) } +.big-520 { color: var(--brand); background: url(img/bg.png) } +.big-521 { color: var(--brand); background: url(img/bg.png) } +.big-522 { color: var(--brand); background: url(img/bg.png) } +.big-523 { color: var(--brand); background: url(img/bg.png) } +.big-524 { color: var(--brand); background: url(img/bg.png) } +.big-525 { color: var(--brand); background: url(img/bg.png) } +.big-526 { color: var(--brand); background: url(img/bg.png) } +.big-527 { color: var(--brand); background: url(img/bg.png) } +.big-528 { color: var(--brand); background: url(img/bg.png) } +.big-529 { color: var(--brand); background: url(img/bg.png) } +.big-530 { color: var(--brand); background: url(img/bg.png) } +.big-531 { color: var(--brand); background: url(img/bg.png) } +.big-532 { color: var(--brand); background: url(img/bg.png) } +.big-533 { color: var(--brand); background: url(img/bg.png) } +.big-534 { color: var(--brand); background: url(img/bg.png) } +.big-535 { color: var(--brand); background: url(img/bg.png) } +.big-536 { color: var(--brand); background: url(img/bg.png) } +.big-537 { color: var(--brand); background: url(img/bg.png) } +.big-538 { color: var(--brand); background: url(img/bg.png) } +.big-539 { color: var(--brand); background: url(img/bg.png) } +.big-540 { color: var(--brand); background: url(img/bg.png) } +.big-541 { color: var(--brand); background: url(img/bg.png) } +.big-542 { color: var(--brand); background: url(img/bg.png) } +.big-543 { color: var(--brand); background: url(img/bg.png) } +.big-544 { color: var(--brand); background: url(img/bg.png) } +.big-545 { color: var(--brand); background: url(img/bg.png) } +.big-546 { color: var(--brand); background: url(img/bg.png) } +.big-547 { color: var(--brand); background: url(img/bg.png) } +.big-548 { color: var(--brand); background: url(img/bg.png) } +.big-549 { color: var(--brand); background: url(img/bg.png) } +.big-550 { color: var(--brand); background: url(img/bg.png) } +.big-551 { color: var(--brand); background: url(img/bg.png) } +.big-552 { color: var(--brand); background: url(img/bg.png) } +.big-553 { color: var(--brand); background: url(img/bg.png) } +.big-554 { color: var(--brand); background: url(img/bg.png) } +.big-555 { color: var(--brand); background: url(img/bg.png) } +.big-556 { color: var(--brand); background: url(img/bg.png) } +.big-557 { color: var(--brand); background: url(img/bg.png) } +.big-558 { color: var(--brand); background: url(img/bg.png) } +.big-559 { color: var(--brand); background: url(img/bg.png) } +.big-560 { color: var(--brand); background: url(img/bg.png) } +.big-561 { color: var(--brand); background: url(img/bg.png) } +.big-562 { color: var(--brand); background: url(img/bg.png) } +.big-563 { color: var(--brand); background: url(img/bg.png) } +.big-564 { color: var(--brand); background: url(img/bg.png) } +.big-565 { color: var(--brand); background: url(img/bg.png) } +.big-566 { color: var(--brand); background: url(img/bg.png) } +.big-567 { color: var(--brand); background: url(img/bg.png) } +.big-568 { color: var(--brand); background: url(img/bg.png) } +.big-569 { color: var(--brand); background: url(img/bg.png) } +.big-570 { color: var(--brand); background: url(img/bg.png) } +.big-571 { color: var(--brand); background: url(img/bg.png) } +.big-572 { color: var(--brand); background: url(img/bg.png) } +.big-573 { color: var(--brand); background: url(img/bg.png) } +.big-574 { color: var(--brand); background: url(img/bg.png) } +.big-575 { color: var(--brand); background: url(img/bg.png) } +.big-576 { color: var(--brand); background: url(img/bg.png) } +.big-577 { color: var(--brand); background: url(img/bg.png) } +.big-578 { color: var(--brand); background: url(img/bg.png) } +.big-579 { color: var(--brand); background: url(img/bg.png) } +.big-580 { color: var(--brand); background: url(img/bg.png) } +.big-581 { color: var(--brand); background: url(img/bg.png) } +.big-582 { color: var(--brand); background: url(img/bg.png) } +.big-583 { color: var(--brand); background: url(img/bg.png) } +.big-584 { color: var(--brand); background: url(img/bg.png) } +.big-585 { color: var(--brand); background: url(img/bg.png) } +.big-586 { color: var(--brand); background: url(img/bg.png) } +.big-587 { color: var(--brand); background: url(img/bg.png) } +.big-588 { color: var(--brand); background: url(img/bg.png) } +.big-589 { color: var(--brand); background: url(img/bg.png) } +.big-590 { color: var(--brand); background: url(img/bg.png) } +.big-591 { color: var(--brand); background: url(img/bg.png) } +.big-592 { color: var(--brand); background: url(img/bg.png) } +.big-593 { color: var(--brand); background: url(img/bg.png) } +.big-594 { color: var(--brand); background: url(img/bg.png) } +.big-595 { color: var(--brand); background: url(img/bg.png) } +.big-596 { color: var(--brand); background: url(img/bg.png) } +.big-597 { color: var(--brand); background: url(img/bg.png) } +.big-598 { color: var(--brand); background: url(img/bg.png) } +.big-599 { color: var(--brand); background: url(img/bg.png) } +.big-600 { color: var(--brand); background: url(img/bg.png) } +.big-601 { color: var(--brand); background: url(img/bg.png) } +.big-602 { color: var(--brand); background: url(img/bg.png) } +.big-603 { color: var(--brand); background: url(img/bg.png) } +.big-604 { color: var(--brand); background: url(img/bg.png) } +.big-605 { color: var(--brand); background: url(img/bg.png) } +.big-606 { color: var(--brand); background: url(img/bg.png) } +.big-607 { color: var(--brand); background: url(img/bg.png) } +.big-608 { color: var(--brand); background: url(img/bg.png) } +.big-609 { color: var(--brand); background: url(img/bg.png) } +.big-610 { color: var(--brand); background: url(img/bg.png) } +.big-611 { color: var(--brand); background: url(img/bg.png) } +.big-612 { color: var(--brand); background: url(img/bg.png) } +.big-613 { color: var(--brand); background: url(img/bg.png) } +.big-614 { color: var(--brand); background: url(img/bg.png) } +.big-615 { color: var(--brand); background: url(img/bg.png) } +.big-616 { color: var(--brand); background: url(img/bg.png) } +.big-617 { color: var(--brand); background: url(img/bg.png) } +.big-618 { color: var(--brand); background: url(img/bg.png) } +.big-619 { color: var(--brand); background: url(img/bg.png) } +.big-620 { color: var(--brand); background: url(img/bg.png) } +.big-621 { color: var(--brand); background: url(img/bg.png) } +.big-622 { color: var(--brand); background: url(img/bg.png) } +.big-623 { color: var(--brand); background: url(img/bg.png) } +.big-624 { color: var(--brand); background: url(img/bg.png) } +.big-625 { color: var(--brand); background: url(img/bg.png) } +.big-626 { color: var(--brand); background: url(img/bg.png) } +.big-627 { color: var(--brand); background: url(img/bg.png) } +.big-628 { color: var(--brand); background: url(img/bg.png) } +.big-629 { color: var(--brand); background: url(img/bg.png) } +.big-630 { color: var(--brand); background: url(img/bg.png) } +.big-631 { color: var(--brand); background: url(img/bg.png) } +.big-632 { color: var(--brand); background: url(img/bg.png) } +.big-633 { color: var(--brand); background: url(img/bg.png) } +.big-634 { color: var(--brand); background: url(img/bg.png) } +.big-635 { color: var(--brand); background: url(img/bg.png) } +.big-636 { color: var(--brand); background: url(img/bg.png) } +.big-637 { color: var(--brand); background: url(img/bg.png) } +.big-638 { color: var(--brand); background: url(img/bg.png) } +.big-639 { color: var(--brand); background: url(img/bg.png) } +.big-640 { color: var(--brand); background: url(img/bg.png) } +.big-641 { color: var(--brand); background: url(img/bg.png) } +.big-642 { color: var(--brand); background: url(img/bg.png) } +.big-643 { color: var(--brand); background: url(img/bg.png) } +.big-644 { color: var(--brand); background: url(img/bg.png) } +.big-645 { color: var(--brand); background: url(img/bg.png) } +.big-646 { color: var(--brand); background: url(img/bg.png) } +.big-647 { color: var(--brand); background: url(img/bg.png) } +.big-648 { color: var(--brand); background: url(img/bg.png) } +.big-649 { color: var(--brand); background: url(img/bg.png) } +.big-650 { color: var(--brand); background: url(img/bg.png) } +.big-651 { color: var(--brand); background: url(img/bg.png) } +.big-652 { color: var(--brand); background: url(img/bg.png) } +.big-653 { color: var(--brand); background: url(img/bg.png) } +.big-654 { color: var(--brand); background: url(img/bg.png) } +.big-655 { color: var(--brand); background: url(img/bg.png) } +.big-656 { color: var(--brand); background: url(img/bg.png) } +.big-657 { color: var(--brand); background: url(img/bg.png) } +.big-658 { color: var(--brand); background: url(img/bg.png) } +.big-659 { color: var(--brand); background: url(img/bg.png) } +.big-660 { color: var(--brand); background: url(img/bg.png) } +.big-661 { color: var(--brand); background: url(img/bg.png) } +.big-662 { color: var(--brand); background: url(img/bg.png) } +.big-663 { color: var(--brand); background: url(img/bg.png) } +.big-664 { color: var(--brand); background: url(img/bg.png) } +.big-665 { color: var(--brand); background: url(img/bg.png) } +.big-666 { color: var(--brand); background: url(img/bg.png) } +.big-667 { color: var(--brand); background: url(img/bg.png) } +.big-668 { color: var(--brand); background: url(img/bg.png) } +.big-669 { color: var(--brand); background: url(img/bg.png) } +.big-670 { color: var(--brand); background: url(img/bg.png) } +.big-671 { color: var(--brand); background: url(img/bg.png) } +.big-672 { color: var(--brand); background: url(img/bg.png) } +.big-673 { color: var(--brand); background: url(img/bg.png) } +.big-674 { color: var(--brand); background: url(img/bg.png) } +.big-675 { color: var(--brand); background: url(img/bg.png) } +.big-676 { color: var(--brand); background: url(img/bg.png) } +.big-677 { color: var(--brand); background: url(img/bg.png) } +.big-678 { color: var(--brand); background: url(img/bg.png) } +.big-679 { color: var(--brand); background: url(img/bg.png) } +.big-680 { color: var(--brand); background: url(img/bg.png) } +.big-681 { color: var(--brand); background: url(img/bg.png) } +.big-682 { color: var(--brand); background: url(img/bg.png) } +.big-683 { color: var(--brand); background: url(img/bg.png) } +.big-684 { color: var(--brand); background: url(img/bg.png) } +.big-685 { color: var(--brand); background: url(img/bg.png) } +.big-686 { color: var(--brand); background: url(img/bg.png) } +.big-687 { color: var(--brand); background: url(img/bg.png) } +.big-688 { color: var(--brand); background: url(img/bg.png) } +.big-689 { color: var(--brand); background: url(img/bg.png) } +.big-690 { color: var(--brand); background: url(img/bg.png) } +.big-691 { color: var(--brand); background: url(img/bg.png) } +.big-692 { color: var(--brand); background: url(img/bg.png) } +.big-693 { color: var(--brand); background: url(img/bg.png) } +.big-694 { color: var(--brand); background: url(img/bg.png) } +.big-695 { color: var(--brand); background: url(img/bg.png) } +.big-696 { color: var(--brand); background: url(img/bg.png) } +.big-697 { color: var(--brand); background: url(img/bg.png) } +.big-698 { color: var(--brand); background: url(img/bg.png) } +.big-699 { color: var(--brand); background: url(img/bg.png) } +.big-700 { color: var(--brand); background: url(img/bg.png) } +.big-701 { color: var(--brand); background: url(img/bg.png) } +.big-702 { color: var(--brand); background: url(img/bg.png) } +.big-703 { color: var(--brand); background: url(img/bg.png) } +.big-704 { color: var(--brand); background: url(img/bg.png) } +.big-705 { color: var(--brand); background: url(img/bg.png) } +.big-706 { color: var(--brand); background: url(img/bg.png) } +.big-707 { color: var(--brand); background: url(img/bg.png) } +.big-708 { color: var(--brand); background: url(img/bg.png) } +.big-709 { color: var(--brand); background: url(img/bg.png) } +.big-710 { color: var(--brand); background: url(img/bg.png) } +.big-711 { color: var(--brand); background: url(img/bg.png) } +.big-712 { color: var(--brand); background: url(img/bg.png) } +.big-713 { color: var(--brand); background: url(img/bg.png) } +.big-714 { color: var(--brand); background: url(img/bg.png) } +.big-715 { color: var(--brand); background: url(img/bg.png) } +.big-716 { color: var(--brand); background: url(img/bg.png) } +.big-717 { color: var(--brand); background: url(img/bg.png) } +.big-718 { color: var(--brand); background: url(img/bg.png) } +.big-719 { color: var(--brand); background: url(img/bg.png) } +.big-720 { color: var(--brand); background: url(img/bg.png) } +.big-721 { color: var(--brand); background: url(img/bg.png) } +.big-722 { color: var(--brand); background: url(img/bg.png) } +.big-723 { color: var(--brand); background: url(img/bg.png) } +.big-724 { color: var(--brand); background: url(img/bg.png) } +.big-725 { color: var(--brand); background: url(img/bg.png) } +.big-726 { color: var(--brand); background: url(img/bg.png) } +.big-727 { color: var(--brand); background: url(img/bg.png) } +.big-728 { color: var(--brand); background: url(img/bg.png) } +.big-729 { color: var(--brand); background: url(img/bg.png) } +.big-730 { color: var(--brand); background: url(img/bg.png) } +.big-731 { color: var(--brand); background: url(img/bg.png) } +.big-732 { color: var(--brand); background: url(img/bg.png) } +.big-733 { color: var(--brand); background: url(img/bg.png) } +.big-734 { color: var(--brand); background: url(img/bg.png) } +.big-735 { color: var(--brand); background: url(img/bg.png) } +.big-736 { color: var(--brand); background: url(img/bg.png) } +.big-737 { color: var(--brand); background: url(img/bg.png) } +.big-738 { color: var(--brand); background: url(img/bg.png) } +.big-739 { color: var(--brand); background: url(img/bg.png) } +.big-740 { color: var(--brand); background: url(img/bg.png) } +.big-741 { color: var(--brand); background: url(img/bg.png) } +.big-742 { color: var(--brand); background: url(img/bg.png) } +.big-743 { color: var(--brand); background: url(img/bg.png) } +.big-744 { color: var(--brand); background: url(img/bg.png) } +.big-745 { color: var(--brand); background: url(img/bg.png) } +.big-746 { color: var(--brand); background: url(img/bg.png) } +.big-747 { color: var(--brand); background: url(img/bg.png) } +.big-748 { color: var(--brand); background: url(img/bg.png) } +.big-749 { color: var(--brand); background: url(img/bg.png) } +.big-750 { color: var(--brand); background: url(img/bg.png) } +.big-751 { color: var(--brand); background: url(img/bg.png) } +.big-752 { color: var(--brand); background: url(img/bg.png) } +.big-753 { color: var(--brand); background: url(img/bg.png) } +.big-754 { color: var(--brand); background: url(img/bg.png) } +.big-755 { color: var(--brand); background: url(img/bg.png) } +.big-756 { color: var(--brand); background: url(img/bg.png) } +.big-757 { color: var(--brand); background: url(img/bg.png) } +.big-758 { color: var(--brand); background: url(img/bg.png) } +.big-759 { color: var(--brand); background: url(img/bg.png) } +.big-760 { color: var(--brand); background: url(img/bg.png) } +.big-761 { color: var(--brand); background: url(img/bg.png) } +.big-762 { color: var(--brand); background: url(img/bg.png) } +.big-763 { color: var(--brand); background: url(img/bg.png) } +.big-764 { color: var(--brand); background: url(img/bg.png) } +.big-765 { color: var(--brand); background: url(img/bg.png) } +.big-766 { color: var(--brand); background: url(img/bg.png) } +.big-767 { color: var(--brand); background: url(img/bg.png) } +.big-768 { color: var(--brand); background: url(img/bg.png) } +.big-769 { color: var(--brand); background: url(img/bg.png) } +.big-770 { color: var(--brand); background: url(img/bg.png) } +.big-771 { color: var(--brand); background: url(img/bg.png) } +.big-772 { color: var(--brand); background: url(img/bg.png) } +.big-773 { color: var(--brand); background: url(img/bg.png) } +.big-774 { color: var(--brand); background: url(img/bg.png) } +.big-775 { color: var(--brand); background: url(img/bg.png) } +.big-776 { color: var(--brand); background: url(img/bg.png) } +.big-777 { color: var(--brand); background: url(img/bg.png) } +.big-778 { color: var(--brand); background: url(img/bg.png) } +.big-779 { color: var(--brand); background: url(img/bg.png) } +.big-780 { color: var(--brand); background: url(img/bg.png) } +.big-781 { color: var(--brand); background: url(img/bg.png) } +.big-782 { color: var(--brand); background: url(img/bg.png) } +.big-783 { color: var(--brand); background: url(img/bg.png) } +.big-784 { color: var(--brand); background: url(img/bg.png) } +.big-785 { color: var(--brand); background: url(img/bg.png) } +.big-786 { color: var(--brand); background: url(img/bg.png) } +.big-787 { color: var(--brand); background: url(img/bg.png) } +.big-788 { color: var(--brand); background: url(img/bg.png) } +.big-789 { color: var(--brand); background: url(img/bg.png) } +.big-790 { color: var(--brand); background: url(img/bg.png) } +.big-791 { color: var(--brand); background: url(img/bg.png) } +.big-792 { color: var(--brand); background: url(img/bg.png) } +.big-793 { color: var(--brand); background: url(img/bg.png) } +.big-794 { color: var(--brand); background: url(img/bg.png) } +.big-795 { color: var(--brand); background: url(img/bg.png) } +.big-796 { color: var(--brand); background: url(img/bg.png) } +.big-797 { color: var(--brand); background: url(img/bg.png) } +.big-798 { color: var(--brand); background: url(img/bg.png) } +.big-799 { color: var(--brand); background: url(img/bg.png) } +.big-800 { color: var(--brand); background: url(img/bg.png) } +.big-801 { color: var(--brand); background: url(img/bg.png) } +.big-802 { color: var(--brand); background: url(img/bg.png) } +.big-803 { color: var(--brand); background: url(img/bg.png) } +.big-804 { color: var(--brand); background: url(img/bg.png) } +.big-805 { color: var(--brand); background: url(img/bg.png) } +.big-806 { color: var(--brand); background: url(img/bg.png) } +.big-807 { color: var(--brand); background: url(img/bg.png) } +.big-808 { color: var(--brand); background: url(img/bg.png) } +.big-809 { color: var(--brand); background: url(img/bg.png) } +.big-810 { color: var(--brand); background: url(img/bg.png) } +.big-811 { color: var(--brand); background: url(img/bg.png) } +.big-812 { color: var(--brand); background: url(img/bg.png) } +.big-813 { color: var(--brand); background: url(img/bg.png) } +.big-814 { color: var(--brand); background: url(img/bg.png) } +.big-815 { color: var(--brand); background: url(img/bg.png) } +.big-816 { color: var(--brand); background: url(img/bg.png) } +.big-817 { color: var(--brand); background: url(img/bg.png) } +.big-818 { color: var(--brand); background: url(img/bg.png) } +.big-819 { color: var(--brand); background: url(img/bg.png) } +.big-820 { color: var(--brand); background: url(img/bg.png) } +.big-821 { color: var(--brand); background: url(img/bg.png) } +.big-822 { color: var(--brand); background: url(img/bg.png) } +.big-823 { color: var(--brand); background: url(img/bg.png) } +.big-824 { color: var(--brand); background: url(img/bg.png) } +.big-825 { color: var(--brand); background: url(img/bg.png) } +.big-826 { color: var(--brand); background: url(img/bg.png) } +.big-827 { color: var(--brand); background: url(img/bg.png) } +.big-828 { color: var(--brand); background: url(img/bg.png) } +.big-829 { color: var(--brand); background: url(img/bg.png) } +.big-830 { color: var(--brand); background: url(img/bg.png) } +.big-831 { color: var(--brand); background: url(img/bg.png) } +.big-832 { color: var(--brand); background: url(img/bg.png) } +.big-833 { color: var(--brand); background: url(img/bg.png) } +.big-834 { color: var(--brand); background: url(img/bg.png) } +.big-835 { color: var(--brand); background: url(img/bg.png) } +.big-836 { color: var(--brand); background: url(img/bg.png) } +.big-837 { color: var(--brand); background: url(img/bg.png) } +.big-838 { color: var(--brand); background: url(img/bg.png) } +.big-839 { color: var(--brand); background: url(img/bg.png) } +.big-840 { color: var(--brand); background: url(img/bg.png) } +.big-841 { color: var(--brand); background: url(img/bg.png) } +.big-842 { color: var(--brand); background: url(img/bg.png) } +.big-843 { color: var(--brand); background: url(img/bg.png) } +.big-844 { color: var(--brand); background: url(img/bg.png) } +.big-845 { color: var(--brand); background: url(img/bg.png) } +.big-846 { color: var(--brand); background: url(img/bg.png) } +.big-847 { color: var(--brand); background: url(img/bg.png) } +.big-848 { color: var(--brand); background: url(img/bg.png) } +.big-849 { color: var(--brand); background: url(img/bg.png) } +.big-850 { color: var(--brand); background: url(img/bg.png) } +.big-851 { color: var(--brand); background: url(img/bg.png) } +.big-852 { color: var(--brand); background: url(img/bg.png) } +.big-853 { color: var(--brand); background: url(img/bg.png) } +.big-854 { color: var(--brand); background: url(img/bg.png) } +.big-855 { color: var(--brand); background: url(img/bg.png) } +.big-856 { color: var(--brand); background: url(img/bg.png) } +.big-857 { color: var(--brand); background: url(img/bg.png) } +.big-858 { color: var(--brand); background: url(img/bg.png) } +.big-859 { color: var(--brand); background: url(img/bg.png) } +.big-860 { color: var(--brand); background: url(img/bg.png) } +.big-861 { color: var(--brand); background: url(img/bg.png) } +.big-862 { color: var(--brand); background: url(img/bg.png) } +.big-863 { color: var(--brand); background: url(img/bg.png) } +.big-864 { color: var(--brand); background: url(img/bg.png) } +.big-865 { color: var(--brand); background: url(img/bg.png) } +.big-866 { color: var(--brand); background: url(img/bg.png) } +.big-867 { color: var(--brand); background: url(img/bg.png) } +.big-868 { color: var(--brand); background: url(img/bg.png) } +.big-869 { color: var(--brand); background: url(img/bg.png) } +.big-870 { color: var(--brand); background: url(img/bg.png) } +.big-871 { color: var(--brand); background: url(img/bg.png) } +.big-872 { color: var(--brand); background: url(img/bg.png) } +.big-873 { color: var(--brand); background: url(img/bg.png) } +.big-874 { color: var(--brand); background: url(img/bg.png) } +.big-875 { color: var(--brand); background: url(img/bg.png) } +.big-876 { color: var(--brand); background: url(img/bg.png) } +.big-877 { color: var(--brand); background: url(img/bg.png) } +.big-878 { color: var(--brand); background: url(img/bg.png) } +.big-879 { color: var(--brand); background: url(img/bg.png) } +.big-880 { color: var(--brand); background: url(img/bg.png) } +.big-881 { color: var(--brand); background: url(img/bg.png) } +.big-882 { color: var(--brand); background: url(img/bg.png) } +.big-883 { color: var(--brand); background: url(img/bg.png) } +.big-884 { color: var(--brand); background: url(img/bg.png) } +.big-885 { color: var(--brand); background: url(img/bg.png) } +.big-886 { color: var(--brand); background: url(img/bg.png) } +.big-887 { color: var(--brand); background: url(img/bg.png) } +.big-888 { color: var(--brand); background: url(img/bg.png) } +.big-889 { color: var(--brand); background: url(img/bg.png) } +.big-890 { color: var(--brand); background: url(img/bg.png) } +.big-891 { color: var(--brand); background: url(img/bg.png) } +.big-892 { color: var(--brand); background: url(img/bg.png) } +.big-893 { color: var(--brand); background: url(img/bg.png) } +.big-894 { color: var(--brand); background: url(img/bg.png) } +.big-895 { color: var(--brand); background: url(img/bg.png) } +.big-896 { color: var(--brand); background: url(img/bg.png) } +.big-897 { color: var(--brand); background: url(img/bg.png) } +.big-898 { color: var(--brand); background: url(img/bg.png) } +.big-899 { color: var(--brand); background: url(img/bg.png) } +.big-900 { color: var(--brand); background: url(img/bg.png) } +.big-901 { color: var(--brand); background: url(img/bg.png) } +.big-902 { color: var(--brand); background: url(img/bg.png) } +.big-903 { color: var(--brand); background: url(img/bg.png) } +.big-904 { color: var(--brand); background: url(img/bg.png) } +.big-905 { color: var(--brand); background: url(img/bg.png) } +.big-906 { color: var(--brand); background: url(img/bg.png) } +.big-907 { color: var(--brand); background: url(img/bg.png) } +.big-908 { color: var(--brand); background: url(img/bg.png) } +.big-909 { color: var(--brand); background: url(img/bg.png) } +.big-910 { color: var(--brand); background: url(img/bg.png) } +.big-911 { color: var(--brand); background: url(img/bg.png) } +.big-912 { color: var(--brand); background: url(img/bg.png) } +.big-913 { color: var(--brand); background: url(img/bg.png) } +.big-914 { color: var(--brand); background: url(img/bg.png) } +.big-915 { color: var(--brand); background: url(img/bg.png) } +.big-916 { color: var(--brand); background: url(img/bg.png) } +.big-917 { color: var(--brand); background: url(img/bg.png) } +.big-918 { color: var(--brand); background: url(img/bg.png) } +.big-919 { color: var(--brand); background: url(img/bg.png) } +.big-920 { color: var(--brand); background: url(img/bg.png) } +.big-921 { color: var(--brand); background: url(img/bg.png) } +.big-922 { color: var(--brand); background: url(img/bg.png) } +.big-923 { color: var(--brand); background: url(img/bg.png) } +.big-924 { color: var(--brand); background: url(img/bg.png) } +.big-925 { color: var(--brand); background: url(img/bg.png) } +.big-926 { color: var(--brand); background: url(img/bg.png) } +.big-927 { color: var(--brand); background: url(img/bg.png) } +.big-928 { color: var(--brand); background: url(img/bg.png) } +.big-929 { color: var(--brand); background: url(img/bg.png) } +.big-930 { color: var(--brand); background: url(img/bg.png) } +.big-931 { color: var(--brand); background: url(img/bg.png) } +.big-932 { color: var(--brand); background: url(img/bg.png) } +.big-933 { color: var(--brand); background: url(img/bg.png) } +.big-934 { color: var(--brand); background: url(img/bg.png) } +.big-935 { color: var(--brand); background: url(img/bg.png) } +.big-936 { color: var(--brand); background: url(img/bg.png) } +.big-937 { color: var(--brand); background: url(img/bg.png) } +.big-938 { color: var(--brand); background: url(img/bg.png) } +.big-939 { color: var(--brand); background: url(img/bg.png) } +.big-940 { color: var(--brand); background: url(img/bg.png) } +.big-941 { color: var(--brand); background: url(img/bg.png) } +.big-942 { color: var(--brand); background: url(img/bg.png) } +.big-943 { color: var(--brand); background: url(img/bg.png) } +.big-944 { color: var(--brand); background: url(img/bg.png) } +.big-945 { color: var(--brand); background: url(img/bg.png) } +.big-946 { color: var(--brand); background: url(img/bg.png) } +.big-947 { color: var(--brand); background: url(img/bg.png) } +.big-948 { color: var(--brand); background: url(img/bg.png) } +.big-949 { color: var(--brand); background: url(img/bg.png) } +.big-950 { color: var(--brand); background: url(img/bg.png) } +.big-951 { color: var(--brand); background: url(img/bg.png) } +.big-952 { color: var(--brand); background: url(img/bg.png) } +.big-953 { color: var(--brand); background: url(img/bg.png) } +.big-954 { color: var(--brand); background: url(img/bg.png) } +.big-955 { color: var(--brand); background: url(img/bg.png) } +.big-956 { color: var(--brand); background: url(img/bg.png) } +.big-957 { color: var(--brand); background: url(img/bg.png) } +.big-958 { color: var(--brand); background: url(img/bg.png) } +.big-959 { color: var(--brand); background: url(img/bg.png) } +.big-960 { color: var(--brand); background: url(img/bg.png) } +.big-961 { color: var(--brand); background: url(img/bg.png) } +.big-962 { color: var(--brand); background: url(img/bg.png) } +.big-963 { color: var(--brand); background: url(img/bg.png) } +.big-964 { color: var(--brand); background: url(img/bg.png) } +.big-965 { color: var(--brand); background: url(img/bg.png) } +.big-966 { color: var(--brand); background: url(img/bg.png) } +.big-967 { color: var(--brand); background: url(img/bg.png) } +.big-968 { color: var(--brand); background: url(img/bg.png) } +.big-969 { color: var(--brand); background: url(img/bg.png) } +.big-970 { color: var(--brand); background: url(img/bg.png) } +.big-971 { color: var(--brand); background: url(img/bg.png) } +.big-972 { color: var(--brand); background: url(img/bg.png) } +.big-973 { color: var(--brand); background: url(img/bg.png) } +.big-974 { color: var(--brand); background: url(img/bg.png) } +.big-975 { color: var(--brand); background: url(img/bg.png) } +.big-976 { color: var(--brand); background: url(img/bg.png) } +.big-977 { color: var(--brand); background: url(img/bg.png) } +.big-978 { color: var(--brand); background: url(img/bg.png) } +.big-979 { color: var(--brand); background: url(img/bg.png) } +.big-980 { color: var(--brand); background: url(img/bg.png) } +.big-981 { color: var(--brand); background: url(img/bg.png) } +.big-982 { color: var(--brand); background: url(img/bg.png) } +.big-983 { color: var(--brand); background: url(img/bg.png) } +.big-984 { color: var(--brand); background: url(img/bg.png) } +.big-985 { color: var(--brand); background: url(img/bg.png) } +.big-986 { color: var(--brand); background: url(img/bg.png) } +.big-987 { color: var(--brand); background: url(img/bg.png) } +.big-988 { color: var(--brand); background: url(img/bg.png) } +.big-989 { color: var(--brand); background: url(img/bg.png) } +.big-990 { color: var(--brand); background: url(img/bg.png) } +.big-991 { color: var(--brand); background: url(img/bg.png) } +.big-992 { color: var(--brand); background: url(img/bg.png) } +.big-993 { color: var(--brand); background: url(img/bg.png) } +.big-994 { color: var(--brand); background: url(img/bg.png) } +.big-995 { color: var(--brand); background: url(img/bg.png) } +.big-996 { color: var(--brand); background: url(img/bg.png) } +.big-997 { color: var(--brand); background: url(img/bg.png) } +.big-998 { color: var(--brand); background: url(img/bg.png) } +.big-999 { color: var(--brand); background: url(img/bg.png) } +.big-1000 { color: var(--brand); background: url(img/bg.png) } +.big-1001 { color: var(--brand); background: url(img/bg.png) } +.big-1002 { color: var(--brand); background: url(img/bg.png) } +.big-1003 { color: var(--brand); background: url(img/bg.png) } +.big-1004 { color: var(--brand); background: url(img/bg.png) } +.big-1005 { color: var(--brand); background: url(img/bg.png) } +.big-1006 { color: var(--brand); background: url(img/bg.png) } +.big-1007 { color: var(--brand); background: url(img/bg.png) } +.big-1008 { color: var(--brand); background: url(img/bg.png) } +.big-1009 { color: var(--brand); background: url(img/bg.png) } +.big-1010 { color: var(--brand); background: url(img/bg.png) } +.big-1011 { color: var(--brand); background: url(img/bg.png) } +.big-1012 { color: var(--brand); background: url(img/bg.png) } +.big-1013 { color: var(--brand); background: url(img/bg.png) } +.big-1014 { color: var(--brand); background: url(img/bg.png) } +.big-1015 { color: var(--brand); background: url(img/bg.png) } +.big-1016 { color: var(--brand); background: url(img/bg.png) } +.big-1017 { color: var(--brand); background: url(img/bg.png) } +.big-1018 { color: var(--brand); background: url(img/bg.png) } +.big-1019 { color: var(--brand); background: url(img/bg.png) } +.big-1020 { color: var(--brand); background: url(img/bg.png) } +.big-1021 { color: var(--brand); background: url(img/bg.png) } +.big-1022 { color: var(--brand); background: url(img/bg.png) } +.big-1023 { color: var(--brand); background: url(img/bg.png) } +.big-1024 { color: var(--brand); background: url(img/bg.png) } +.big-1025 { color: var(--brand); background: url(img/bg.png) } +.big-1026 { color: var(--brand); background: url(img/bg.png) } +.big-1027 { color: var(--brand); background: url(img/bg.png) } +.big-1028 { color: var(--brand); background: url(img/bg.png) } +.big-1029 { color: var(--brand); background: url(img/bg.png) } +.big-1030 { color: var(--brand); background: url(img/bg.png) } +.big-1031 { color: var(--brand); background: url(img/bg.png) } +.big-1032 { color: var(--brand); background: url(img/bg.png) } +.big-1033 { color: var(--brand); background: url(img/bg.png) } +.big-1034 { color: var(--brand); background: url(img/bg.png) } +.big-1035 { color: var(--brand); background: url(img/bg.png) } +.big-1036 { color: var(--brand); background: url(img/bg.png) } +.big-1037 { color: var(--brand); background: url(img/bg.png) } +.big-1038 { color: var(--brand); background: url(img/bg.png) } +.big-1039 { color: var(--brand); background: url(img/bg.png) } +.big-1040 { color: var(--brand); background: url(img/bg.png) } +.big-1041 { color: var(--brand); background: url(img/bg.png) } +.big-1042 { color: var(--brand); background: url(img/bg.png) } +.big-1043 { color: var(--brand); background: url(img/bg.png) } +.big-1044 { color: var(--brand); background: url(img/bg.png) } +.big-1045 { color: var(--brand); background: url(img/bg.png) } +.big-1046 { color: var(--brand); background: url(img/bg.png) } +.big-1047 { color: var(--brand); background: url(img/bg.png) } +.big-1048 { color: var(--brand); background: url(img/bg.png) } +.big-1049 { color: var(--brand); background: url(img/bg.png) } +.big-1050 { color: var(--brand); background: url(img/bg.png) } +.big-1051 { color: var(--brand); background: url(img/bg.png) } +.big-1052 { color: var(--brand); background: url(img/bg.png) } +.big-1053 { color: var(--brand); background: url(img/bg.png) } +.big-1054 { color: var(--brand); background: url(img/bg.png) } +.big-1055 { color: var(--brand); background: url(img/bg.png) } +.big-1056 { color: var(--brand); background: url(img/bg.png) } +.big-1057 { color: var(--brand); background: url(img/bg.png) } +.big-1058 { color: var(--brand); background: url(img/bg.png) } +.big-1059 { color: var(--brand); background: url(img/bg.png) } +.big-1060 { color: var(--brand); background: url(img/bg.png) } +.big-1061 { color: var(--brand); background: url(img/bg.png) } +.big-1062 { color: var(--brand); background: url(img/bg.png) } +.big-1063 { color: var(--brand); background: url(img/bg.png) } +.big-1064 { color: var(--brand); background: url(img/bg.png) } +.big-1065 { color: var(--brand); background: url(img/bg.png) } +.big-1066 { color: var(--brand); background: url(img/bg.png) } +.big-1067 { color: var(--brand); background: url(img/bg.png) } +.big-1068 { color: var(--brand); background: url(img/bg.png) } +.big-1069 { color: var(--brand); background: url(img/bg.png) } +.big-1070 { color: var(--brand); background: url(img/bg.png) } +.big-1071 { color: var(--brand); background: url(img/bg.png) } +.big-1072 { color: var(--brand); background: url(img/bg.png) } +.big-1073 { color: var(--brand); background: url(img/bg.png) } +.big-1074 { color: var(--brand); background: url(img/bg.png) } +.big-1075 { color: var(--brand); background: url(img/bg.png) } +.big-1076 { color: var(--brand); background: url(img/bg.png) } +.big-1077 { color: var(--brand); background: url(img/bg.png) } +.big-1078 { color: var(--brand); background: url(img/bg.png) } +.big-1079 { color: var(--brand); background: url(img/bg.png) } +.big-1080 { color: var(--brand); background: url(img/bg.png) } +.big-1081 { color: var(--brand); background: url(img/bg.png) } +.big-1082 { color: var(--brand); background: url(img/bg.png) } +.big-1083 { color: var(--brand); background: url(img/bg.png) } +.big-1084 { color: var(--brand); background: url(img/bg.png) } +.big-1085 { color: var(--brand); background: url(img/bg.png) } +.big-1086 { color: var(--brand); background: url(img/bg.png) } +.big-1087 { color: var(--brand); background: url(img/bg.png) } +.big-1088 { color: var(--brand); background: url(img/bg.png) } +.big-1089 { color: var(--brand); background: url(img/bg.png) } +.big-1090 { color: var(--brand); background: url(img/bg.png) } +.big-1091 { color: var(--brand); background: url(img/bg.png) } +.big-1092 { color: var(--brand); background: url(img/bg.png) } +.big-1093 { color: var(--brand); background: url(img/bg.png) } +.big-1094 { color: var(--brand); background: url(img/bg.png) } +.big-1095 { color: var(--brand); background: url(img/bg.png) } +.big-1096 { color: var(--brand); background: url(img/bg.png) } +.big-1097 { color: var(--brand); background: url(img/bg.png) } +.big-1098 { color: var(--brand); background: url(img/bg.png) } +.big-1099 { color: var(--brand); background: url(img/bg.png) } +.big-1100 { color: var(--brand); background: url(img/bg.png) } +.big-1101 { color: var(--brand); background: url(img/bg.png) } +.big-1102 { color: var(--brand); background: url(img/bg.png) } +.big-1103 { color: var(--brand); background: url(img/bg.png) } +.big-1104 { color: var(--brand); background: url(img/bg.png) } +.big-1105 { color: var(--brand); background: url(img/bg.png) } +.big-1106 { color: var(--brand); background: url(img/bg.png) } +.big-1107 { color: var(--brand); background: url(img/bg.png) } +.big-1108 { color: var(--brand); background: url(img/bg.png) } +.big-1109 { color: var(--brand); background: url(img/bg.png) } +.big-1110 { color: var(--brand); background: url(img/bg.png) } +.big-1111 { color: var(--brand); background: url(img/bg.png) } +.big-1112 { color: var(--brand); background: url(img/bg.png) } +.big-1113 { color: var(--brand); background: url(img/bg.png) } +.big-1114 { color: var(--brand); background: url(img/bg.png) } +.big-1115 { color: var(--brand); background: url(img/bg.png) } +.big-1116 { color: var(--brand); background: url(img/bg.png) } +.big-1117 { color: var(--brand); background: url(img/bg.png) } +.big-1118 { color: var(--brand); background: url(img/bg.png) } +.big-1119 { color: var(--brand); background: url(img/bg.png) } +.big-1120 { color: var(--brand); background: url(img/bg.png) } +.big-1121 { color: var(--brand); background: url(img/bg.png) } +.big-1122 { color: var(--brand); background: url(img/bg.png) } +.big-1123 { color: var(--brand); background: url(img/bg.png) } +.big-1124 { color: var(--brand); background: url(img/bg.png) } +.big-1125 { color: var(--brand); background: url(img/bg.png) } +.big-1126 { color: var(--brand); background: url(img/bg.png) } +.big-1127 { color: var(--brand); background: url(img/bg.png) } +.big-1128 { color: var(--brand); background: url(img/bg.png) } +.big-1129 { color: var(--brand); background: url(img/bg.png) } +.big-1130 { color: var(--brand); background: url(img/bg.png) } +.big-1131 { color: var(--brand); background: url(img/bg.png) } +.big-1132 { color: var(--brand); background: url(img/bg.png) } +.big-1133 { color: var(--brand); background: url(img/bg.png) } +.big-1134 { color: var(--brand); background: url(img/bg.png) } +.big-1135 { color: var(--brand); background: url(img/bg.png) } +.big-1136 { color: var(--brand); background: url(img/bg.png) } +.big-1137 { color: var(--brand); background: url(img/bg.png) } +.big-1138 { color: var(--brand); background: url(img/bg.png) } +.big-1139 { color: var(--brand); background: url(img/bg.png) } +.big-1140 { color: var(--brand); background: url(img/bg.png) } +.big-1141 { color: var(--brand); background: url(img/bg.png) } +.big-1142 { color: var(--brand); background: url(img/bg.png) } +.big-1143 { color: var(--brand); background: url(img/bg.png) } +.big-1144 { color: var(--brand); background: url(img/bg.png) } +.big-1145 { color: var(--brand); background: url(img/bg.png) } +.big-1146 { color: var(--brand); background: url(img/bg.png) } +.big-1147 { color: var(--brand); background: url(img/bg.png) } +.big-1148 { color: var(--brand); background: url(img/bg.png) } +.big-1149 { color: var(--brand); background: url(img/bg.png) } +.big-1150 { color: var(--brand); background: url(img/bg.png) } +.big-1151 { color: var(--brand); background: url(img/bg.png) } +.big-1152 { color: var(--brand); background: url(img/bg.png) } +.big-1153 { color: var(--brand); background: url(img/bg.png) } +.big-1154 { color: var(--brand); background: url(img/bg.png) } +.big-1155 { color: var(--brand); background: url(img/bg.png) } +.big-1156 { color: var(--brand); background: url(img/bg.png) } +.big-1157 { color: var(--brand); background: url(img/bg.png) } +.big-1158 { color: var(--brand); background: url(img/bg.png) } +.big-1159 { color: var(--brand); background: url(img/bg.png) } +.big-1160 { color: var(--brand); background: url(img/bg.png) } +.big-1161 { color: var(--brand); background: url(img/bg.png) } +.big-1162 { color: var(--brand); background: url(img/bg.png) } +.big-1163 { color: var(--brand); background: url(img/bg.png) } +.big-1164 { color: var(--brand); background: url(img/bg.png) } +.big-1165 { color: var(--brand); background: url(img/bg.png) } +.big-1166 { color: var(--brand); background: url(img/bg.png) } +.big-1167 { color: var(--brand); background: url(img/bg.png) } +.big-1168 { color: var(--brand); background: url(img/bg.png) } +.big-1169 { color: var(--brand); background: url(img/bg.png) } +.big-1170 { color: var(--brand); background: url(img/bg.png) } +.big-1171 { color: var(--brand); background: url(img/bg.png) } +.big-1172 { color: var(--brand); background: url(img/bg.png) } +.big-1173 { color: var(--brand); background: url(img/bg.png) } +.big-1174 { color: var(--brand); background: url(img/bg.png) } +.big-1175 { color: var(--brand); background: url(img/bg.png) } +.big-1176 { color: var(--brand); background: url(img/bg.png) } +.big-1177 { color: var(--brand); background: url(img/bg.png) } +.big-1178 { color: var(--brand); background: url(img/bg.png) } +.big-1179 { color: var(--brand); background: url(img/bg.png) } +.big-1180 { color: var(--brand); background: url(img/bg.png) } +.big-1181 { color: var(--brand); background: url(img/bg.png) } +.big-1182 { color: var(--brand); background: url(img/bg.png) } +.big-1183 { color: var(--brand); background: url(img/bg.png) } +.big-1184 { color: var(--brand); background: url(img/bg.png) } +.big-1185 { color: var(--brand); background: url(img/bg.png) } +.big-1186 { color: var(--brand); background: url(img/bg.png) } +.big-1187 { color: var(--brand); background: url(img/bg.png) } +.big-1188 { color: var(--brand); background: url(img/bg.png) } +.big-1189 { color: var(--brand); background: url(img/bg.png) } +.big-1190 { color: var(--brand); background: url(img/bg.png) } +.big-1191 { color: var(--brand); background: url(img/bg.png) } +.big-1192 { color: var(--brand); background: url(img/bg.png) } +.big-1193 { color: var(--brand); background: url(img/bg.png) } +.big-1194 { color: var(--brand); background: url(img/bg.png) } +.big-1195 { color: var(--brand); background: url(img/bg.png) } +.big-1196 { color: var(--brand); background: url(img/bg.png) } +.big-1197 { color: var(--brand); background: url(img/bg.png) } +.big-1198 { color: var(--brand); background: url(img/bg.png) } +.big-1199 { color: var(--brand); background: url(img/bg.png) } +.big-1200 { color: var(--brand); background: url(img/bg.png) } +.big-1201 { color: var(--brand); background: url(img/bg.png) } +.big-1202 { color: var(--brand); background: url(img/bg.png) } +.big-1203 { color: var(--brand); background: url(img/bg.png) } +.big-1204 { color: var(--brand); background: url(img/bg.png) } +.big-1205 { color: var(--brand); background: url(img/bg.png) } +.big-1206 { color: var(--brand); background: url(img/bg.png) } +.big-1207 { color: var(--brand); background: url(img/bg.png) } +.big-1208 { color: var(--brand); background: url(img/bg.png) } +.big-1209 { color: var(--brand); background: url(img/bg.png) } +.big-1210 { color: var(--brand); background: url(img/bg.png) } +.big-1211 { color: var(--brand); background: url(img/bg.png) } +.big-1212 { color: var(--brand); background: url(img/bg.png) } +.big-1213 { color: var(--brand); background: url(img/bg.png) } +.big-1214 { color: var(--brand); background: url(img/bg.png) } +.big-1215 { color: var(--brand); background: url(img/bg.png) } +.big-1216 { color: var(--brand); background: url(img/bg.png) } +.big-1217 { color: var(--brand); background: url(img/bg.png) } +.big-1218 { color: var(--brand); background: url(img/bg.png) } +.big-1219 { color: var(--brand); background: url(img/bg.png) } +.big-1220 { color: var(--brand); background: url(img/bg.png) } +.big-1221 { color: var(--brand); background: url(img/bg.png) } +.big-1222 { color: var(--brand); background: url(img/bg.png) } +.big-1223 { color: var(--brand); background: url(img/bg.png) } +.big-1224 { color: var(--brand); background: url(img/bg.png) } +.big-1225 { color: var(--brand); background: url(img/bg.png) } +.big-1226 { color: var(--brand); background: url(img/bg.png) } +.big-1227 { color: var(--brand); background: url(img/bg.png) } +.big-1228 { color: var(--brand); background: url(img/bg.png) } +.big-1229 { color: var(--brand); background: url(img/bg.png) } +.big-1230 { color: var(--brand); background: url(img/bg.png) } +.big-1231 { color: var(--brand); background: url(img/bg.png) } +.big-1232 { color: var(--brand); background: url(img/bg.png) } +.big-1233 { color: var(--brand); background: url(img/bg.png) } +.big-1234 { color: var(--brand); background: url(img/bg.png) } +.big-1235 { color: var(--brand); background: url(img/bg.png) } +.big-1236 { color: var(--brand); background: url(img/bg.png) } +.big-1237 { color: var(--brand); background: url(img/bg.png) } +.big-1238 { color: var(--brand); background: url(img/bg.png) } +.big-1239 { color: var(--brand); background: url(img/bg.png) } +.big-1240 { color: var(--brand); background: url(img/bg.png) } +.big-1241 { color: var(--brand); background: url(img/bg.png) } +.big-1242 { color: var(--brand); background: url(img/bg.png) } +.big-1243 { color: var(--brand); background: url(img/bg.png) } +.big-1244 { color: var(--brand); background: url(img/bg.png) } +.big-1245 { color: var(--brand); background: url(img/bg.png) } +.big-1246 { color: var(--brand); background: url(img/bg.png) } +.big-1247 { color: var(--brand); background: url(img/bg.png) } +.big-1248 { color: var(--brand); background: url(img/bg.png) } +.big-1249 { color: var(--brand); background: url(img/bg.png) } +.big-1250 { color: var(--brand); background: url(img/bg.png) } +.big-1251 { color: var(--brand); background: url(img/bg.png) } +.big-1252 { color: var(--brand); background: url(img/bg.png) } +.big-1253 { color: var(--brand); background: url(img/bg.png) } +.big-1254 { color: var(--brand); background: url(img/bg.png) } +.big-1255 { color: var(--brand); background: url(img/bg.png) } +.big-1256 { color: var(--brand); background: url(img/bg.png) } +.big-1257 { color: var(--brand); background: url(img/bg.png) } +.big-1258 { color: var(--brand); background: url(img/bg.png) } +.big-1259 { color: var(--brand); background: url(img/bg.png) } +.big-1260 { color: var(--brand); background: url(img/bg.png) } +.big-1261 { color: var(--brand); background: url(img/bg.png) } +.big-1262 { color: var(--brand); background: url(img/bg.png) } +.big-1263 { color: var(--brand); background: url(img/bg.png) } +.big-1264 { color: var(--brand); background: url(img/bg.png) } +.big-1265 { color: var(--brand); background: url(img/bg.png) } +.big-1266 { color: var(--brand); background: url(img/bg.png) } +.big-1267 { color: var(--brand); background: url(img/bg.png) } +.big-1268 { color: var(--brand); background: url(img/bg.png) } +.big-1269 { color: var(--brand); background: url(img/bg.png) } +.big-1270 { color: var(--brand); background: url(img/bg.png) } +.big-1271 { color: var(--brand); background: url(img/bg.png) } +.big-1272 { color: var(--brand); background: url(img/bg.png) } +.big-1273 { color: var(--brand); background: url(img/bg.png) } +.big-1274 { color: var(--brand); background: url(img/bg.png) } +.big-1275 { color: var(--brand); background: url(img/bg.png) } +.big-1276 { color: var(--brand); background: url(img/bg.png) } +.big-1277 { color: var(--brand); background: url(img/bg.png) } +.big-1278 { color: var(--brand); background: url(img/bg.png) } +.big-1279 { color: var(--brand); background: url(img/bg.png) } +.big-1280 { color: var(--brand); background: url(img/bg.png) } +.big-1281 { color: var(--brand); background: url(img/bg.png) } +.big-1282 { color: var(--brand); background: url(img/bg.png) } +.big-1283 { color: var(--brand); background: url(img/bg.png) } +.big-1284 { color: var(--brand); background: url(img/bg.png) } +.big-1285 { color: var(--brand); background: url(img/bg.png) } +.big-1286 { color: var(--brand); background: url(img/bg.png) } +.big-1287 { color: var(--brand); background: url(img/bg.png) } +.big-1288 { color: var(--brand); background: url(img/bg.png) } +.big-1289 { color: var(--brand); background: url(img/bg.png) } +.big-1290 { color: var(--brand); background: url(img/bg.png) } +.big-1291 { color: var(--brand); background: url(img/bg.png) } +.big-1292 { color: var(--brand); background: url(img/bg.png) } +.big-1293 { color: var(--brand); background: url(img/bg.png) } +.big-1294 { color: var(--brand); background: url(img/bg.png) } +.big-1295 { color: var(--brand); background: url(img/bg.png) } +.big-1296 { color: var(--brand); background: url(img/bg.png) } +.big-1297 { color: var(--brand); background: url(img/bg.png) } +.big-1298 { color: var(--brand); background: url(img/bg.png) } +.big-1299 { color: var(--brand); background: url(img/bg.png) } +.big-1300 { color: var(--brand); background: url(img/bg.png) } +.big-1301 { color: var(--brand); background: url(img/bg.png) } +.big-1302 { color: var(--brand); background: url(img/bg.png) } +.big-1303 { color: var(--brand); background: url(img/bg.png) } +.big-1304 { color: var(--brand); background: url(img/bg.png) } +.big-1305 { color: var(--brand); background: url(img/bg.png) } +.big-1306 { color: var(--brand); background: url(img/bg.png) } +.big-1307 { color: var(--brand); background: url(img/bg.png) } +.big-1308 { color: var(--brand); background: url(img/bg.png) } +.big-1309 { color: var(--brand); background: url(img/bg.png) } +.big-1310 { color: var(--brand); background: url(img/bg.png) } +.big-1311 { color: var(--brand); background: url(img/bg.png) } +.big-1312 { color: var(--brand); background: url(img/bg.png) } +.big-1313 { color: var(--brand); background: url(img/bg.png) } +.big-1314 { color: var(--brand); background: url(img/bg.png) } +.big-1315 { color: var(--brand); background: url(img/bg.png) } +.big-1316 { color: var(--brand); background: url(img/bg.png) } +.big-1317 { color: var(--brand); background: url(img/bg.png) } +.big-1318 { color: var(--brand); background: url(img/bg.png) } +.big-1319 { color: var(--brand); background: url(img/bg.png) } +.big-1320 { color: var(--brand); background: url(img/bg.png) } +.big-1321 { color: var(--brand); background: url(img/bg.png) } +.big-1322 { color: var(--brand); background: url(img/bg.png) } +.big-1323 { color: var(--brand); background: url(img/bg.png) } +.big-1324 { color: var(--brand); background: url(img/bg.png) } +.big-1325 { color: var(--brand); background: url(img/bg.png) } +.big-1326 { color: var(--brand); background: url(img/bg.png) } +.big-1327 { color: var(--brand); background: url(img/bg.png) } +.big-1328 { color: var(--brand); background: url(img/bg.png) } +.big-1329 { color: var(--brand); background: url(img/bg.png) } +.big-1330 { color: var(--brand); background: url(img/bg.png) } +.big-1331 { color: var(--brand); background: url(img/bg.png) } +.big-1332 { color: var(--brand); background: url(img/bg.png) } +.big-1333 { color: var(--brand); background: url(img/bg.png) } +.big-1334 { color: var(--brand); background: url(img/bg.png) } +.big-1335 { color: var(--brand); background: url(img/bg.png) } +.big-1336 { color: var(--brand); background: url(img/bg.png) } +.big-1337 { color: var(--brand); background: url(img/bg.png) } +.big-1338 { color: var(--brand); background: url(img/bg.png) } +.big-1339 { color: var(--brand); background: url(img/bg.png) } +.big-1340 { color: var(--brand); background: url(img/bg.png) } +.big-1341 { color: var(--brand); background: url(img/bg.png) } +.big-1342 { color: var(--brand); background: url(img/bg.png) } +.big-1343 { color: var(--brand); background: url(img/bg.png) } +.big-1344 { color: var(--brand); background: url(img/bg.png) } +.big-1345 { color: var(--brand); background: url(img/bg.png) } +.big-1346 { color: var(--brand); background: url(img/bg.png) } +.big-1347 { color: var(--brand); background: url(img/bg.png) } +.big-1348 { color: var(--brand); background: url(img/bg.png) } +.big-1349 { color: var(--brand); background: url(img/bg.png) } +.big-1350 { color: var(--brand); background: url(img/bg.png) } +.big-1351 { color: var(--brand); background: url(img/bg.png) } +.big-1352 { color: var(--brand); background: url(img/bg.png) } +.big-1353 { color: var(--brand); background: url(img/bg.png) } +.big-1354 { color: var(--brand); background: url(img/bg.png) } +.big-1355 { color: var(--brand); background: url(img/bg.png) } +.big-1356 { color: var(--brand); background: url(img/bg.png) } +.big-1357 { color: var(--brand); background: url(img/bg.png) } +.big-1358 { color: var(--brand); background: url(img/bg.png) } +.big-1359 { color: var(--brand); background: url(img/bg.png) } +.big-1360 { color: var(--brand); background: url(img/bg.png) } +.big-1361 { color: var(--brand); background: url(img/bg.png) } +.big-1362 { color: var(--brand); background: url(img/bg.png) } +.big-1363 { color: var(--brand); background: url(img/bg.png) } +.big-1364 { color: var(--brand); background: url(img/bg.png) } +.big-1365 { color: var(--brand); background: url(img/bg.png) } +.big-1366 { color: var(--brand); background: url(img/bg.png) } +.big-1367 { color: var(--brand); background: url(img/bg.png) } +.big-1368 { color: var(--brand); background: url(img/bg.png) } +.big-1369 { color: var(--brand); background: url(img/bg.png) } +.big-1370 { color: var(--brand); background: url(img/bg.png) } +.big-1371 { color: var(--brand); background: url(img/bg.png) } +.big-1372 { color: var(--brand); background: url(img/bg.png) } +.big-1373 { color: var(--brand); background: url(img/bg.png) } +.big-1374 { color: var(--brand); background: url(img/bg.png) } +.big-1375 { color: var(--brand); background: url(img/bg.png) } +.big-1376 { color: var(--brand); background: url(img/bg.png) } +.big-1377 { color: var(--brand); background: url(img/bg.png) } +.big-1378 { color: var(--brand); background: url(img/bg.png) } +.big-1379 { color: var(--brand); background: url(img/bg.png) } +.big-1380 { color: var(--brand); background: url(img/bg.png) } +.big-1381 { color: var(--brand); background: url(img/bg.png) } +.big-1382 { color: var(--brand); background: url(img/bg.png) } +.big-1383 { color: var(--brand); background: url(img/bg.png) } +.big-1384 { color: var(--brand); background: url(img/bg.png) } +.big-1385 { color: var(--brand); background: url(img/bg.png) } +.big-1386 { color: var(--brand); background: url(img/bg.png) } +.big-1387 { color: var(--brand); background: url(img/bg.png) } +.big-1388 { color: var(--brand); background: url(img/bg.png) } +.big-1389 { color: var(--brand); background: url(img/bg.png) } +.big-1390 { color: var(--brand); background: url(img/bg.png) } +.big-1391 { color: var(--brand); background: url(img/bg.png) } +.big-1392 { color: var(--brand); background: url(img/bg.png) } +.big-1393 { color: var(--brand); background: url(img/bg.png) } +.big-1394 { color: var(--brand); background: url(img/bg.png) } +.big-1395 { color: var(--brand); background: url(img/bg.png) } +.big-1396 { color: var(--brand); background: url(img/bg.png) } +.big-1397 { color: var(--brand); background: url(img/bg.png) } +.big-1398 { color: var(--brand); background: url(img/bg.png) } +.big-1399 { color: var(--brand); background: url(img/bg.png) } +.big-1400 { color: var(--brand); background: url(img/bg.png) } +.big-1401 { color: var(--brand); background: url(img/bg.png) } +.big-1402 { color: var(--brand); background: url(img/bg.png) } +.big-1403 { color: var(--brand); background: url(img/bg.png) } +.big-1404 { color: var(--brand); background: url(img/bg.png) } +.big-1405 { color: var(--brand); background: url(img/bg.png) } +.big-1406 { color: var(--brand); background: url(img/bg.png) } +.big-1407 { color: var(--brand); background: url(img/bg.png) } +.big-1408 { color: var(--brand); background: url(img/bg.png) } +.big-1409 { color: var(--brand); background: url(img/bg.png) } +.big-1410 { color: var(--brand); background: url(img/bg.png) } +.big-1411 { color: var(--brand); background: url(img/bg.png) } +.big-1412 { color: var(--brand); background: url(img/bg.png) } +.big-1413 { color: var(--brand); background: url(img/bg.png) } +.big-1414 { color: var(--brand); background: url(img/bg.png) } +.big-1415 { color: var(--brand); background: url(img/bg.png) } +.big-1416 { color: var(--brand); background: url(img/bg.png) } +.big-1417 { color: var(--brand); background: url(img/bg.png) } +.big-1418 { color: var(--brand); background: url(img/bg.png) } +.big-1419 { color: var(--brand); background: url(img/bg.png) } +.big-1420 { color: var(--brand); background: url(img/bg.png) } +.big-1421 { color: var(--brand); background: url(img/bg.png) } +.big-1422 { color: var(--brand); background: url(img/bg.png) } +.big-1423 { color: var(--brand); background: url(img/bg.png) } +.big-1424 { color: var(--brand); background: url(img/bg.png) } +.big-1425 { color: var(--brand); background: url(img/bg.png) } +.big-1426 { color: var(--brand); background: url(img/bg.png) } +.big-1427 { color: var(--brand); background: url(img/bg.png) } +.big-1428 { color: var(--brand); background: url(img/bg.png) } +.big-1429 { color: var(--brand); background: url(img/bg.png) } +.big-1430 { color: var(--brand); background: url(img/bg.png) } +.big-1431 { color: var(--brand); background: url(img/bg.png) } +.big-1432 { color: var(--brand); background: url(img/bg.png) } +.big-1433 { color: var(--brand); background: url(img/bg.png) } +.big-1434 { color: var(--brand); background: url(img/bg.png) } +.big-1435 { color: var(--brand); background: url(img/bg.png) } +.big-1436 { color: var(--brand); background: url(img/bg.png) } +.big-1437 { color: var(--brand); background: url(img/bg.png) } +.big-1438 { color: var(--brand); background: url(img/bg.png) } +.big-1439 { color: var(--brand); background: url(img/bg.png) } +.big-1440 { color: var(--brand); background: url(img/bg.png) } +.big-1441 { color: var(--brand); background: url(img/bg.png) } +.big-1442 { color: var(--brand); background: url(img/bg.png) } +.big-1443 { color: var(--brand); background: url(img/bg.png) } +.big-1444 { color: var(--brand); background: url(img/bg.png) } +.big-1445 { color: var(--brand); background: url(img/bg.png) } +.big-1446 { color: var(--brand); background: url(img/bg.png) } +.big-1447 { color: var(--brand); background: url(img/bg.png) } +.big-1448 { color: var(--brand); background: url(img/bg.png) } +.big-1449 { color: var(--brand); background: url(img/bg.png) } +.big-1450 { color: var(--brand); background: url(img/bg.png) } +.big-1451 { color: var(--brand); background: url(img/bg.png) } +.big-1452 { color: var(--brand); background: url(img/bg.png) } +.big-1453 { color: var(--brand); background: url(img/bg.png) } +.big-1454 { color: var(--brand); background: url(img/bg.png) } +.big-1455 { color: var(--brand); background: url(img/bg.png) } +.big-1456 { color: var(--brand); background: url(img/bg.png) } +.big-1457 { color: var(--brand); background: url(img/bg.png) } +.big-1458 { color: var(--brand); background: url(img/bg.png) } +.big-1459 { color: var(--brand); background: url(img/bg.png) } +.big-1460 { color: var(--brand); background: url(img/bg.png) } +.big-1461 { color: var(--brand); background: url(img/bg.png) } +.big-1462 { color: var(--brand); background: url(img/bg.png) } +.big-1463 { color: var(--brand); background: url(img/bg.png) } +.big-1464 { color: var(--brand); background: url(img/bg.png) } +.big-1465 { color: var(--brand); background: url(img/bg.png) } +.big-1466 { color: var(--brand); background: url(img/bg.png) } +.big-1467 { color: var(--brand); background: url(img/bg.png) } +.big-1468 { color: var(--brand); background: url(img/bg.png) } +.big-1469 { color: var(--brand); background: url(img/bg.png) } +.big-1470 { color: var(--brand); background: url(img/bg.png) } +.big-1471 { color: var(--brand); background: url(img/bg.png) } +.big-1472 { color: var(--brand); background: url(img/bg.png) } +.big-1473 { color: var(--brand); background: url(img/bg.png) } +.big-1474 { color: var(--brand); background: url(img/bg.png) } +.big-1475 { color: var(--brand); background: url(img/bg.png) } +.big-1476 { color: var(--brand); background: url(img/bg.png) } +.big-1477 { color: var(--brand); background: url(img/bg.png) } +.big-1478 { color: var(--brand); background: url(img/bg.png) } +.big-1479 { color: var(--brand); background: url(img/bg.png) } +.big-1480 { color: var(--brand); background: url(img/bg.png) } +.big-1481 { color: var(--brand); background: url(img/bg.png) } +.big-1482 { color: var(--brand); background: url(img/bg.png) } +.big-1483 { color: var(--brand); background: url(img/bg.png) } +.big-1484 { color: var(--brand); background: url(img/bg.png) } +.big-1485 { color: var(--brand); background: url(img/bg.png) } +.big-1486 { color: var(--brand); background: url(img/bg.png) } +.big-1487 { color: var(--brand); background: url(img/bg.png) } +.big-1488 { color: var(--brand); background: url(img/bg.png) } +.big-1489 { color: var(--brand); background: url(img/bg.png) } +.big-1490 { color: var(--brand); background: url(img/bg.png) } +.big-1491 { color: var(--brand); background: url(img/bg.png) } +.big-1492 { color: var(--brand); background: url(img/bg.png) } +.big-1493 { color: var(--brand); background: url(img/bg.png) } +.big-1494 { color: var(--brand); background: url(img/bg.png) } +.big-1495 { color: var(--brand); background: url(img/bg.png) } +.big-1496 { color: var(--brand); background: url(img/bg.png) } +.big-1497 { color: var(--brand); background: url(img/bg.png) } +.big-1498 { color: var(--brand); background: url(img/bg.png) } +.big-1499 { color: var(--brand); background: url(img/bg.png) } diff --git a/parser/src/test-data/web/torture/css/bom.css b/parser/src/test-data/web/torture/css/bom.css new file mode 100644 index 00000000..53fd637b --- /dev/null +++ b/parser/src/test-data/web/torture/css/bom.css @@ -0,0 +1 @@ +.bom-class { color: red } diff --git a/parser/src/test-data/web/torture/css/disabled.css b/parser/src/test-data/web/torture/css/disabled.css new file mode 100644 index 00000000..bc3d0dc6 --- /dev/null +++ b/parser/src/test-data/web/torture/css/disabled.css @@ -0,0 +1 @@ +.disabled-only { color: red } diff --git a/parser/src/test-data/web/torture/css/fonts.css b/parser/src/test-data/web/torture/css/fonts.css new file mode 100644 index 00000000..d4d5fefe --- /dev/null +++ b/parser/src/test-data/web/torture/css/fonts.css @@ -0,0 +1,38 @@ +/* F01 font faces: multiple src, local(), format(), tech(), unicode-range; and the families that use them */ +@font-face { + font-family: "Inter"; + src: local("Inter"), local(Inter-Regular), + url(../fonts/inter.woff2) format("woff2") tech(variations), + url("../fonts/inter.woff") format("woff"), + url(../fonts/missing.woff) format("woff"), + url(https://fonts.example.com/inter.woff2); + font-weight: 100 900; + unicode-range: U+0000-00FF, U+0131; + font-display: swap; +} +@font-face { font-family: Inter; src: url(../fonts/inter.woff2); font-weight: bold } +@font-face { font-family: 'Roboto Mono'; src: url(../fonts/roboto.woff2) } +@font-face { font-family: "Unused Face"; src: url(../fonts/unused.woff2) } +@font-face { src: url(../fonts/nameless.woff2) } +@font-face { font-family: var(--font); src: url(../fonts/x.woff2) } +@font-face { font-family: "Segoe UI"; src: local("Segoe UI") } +.f1 { font-family: Inter } +.f2 { font-family: "Inter" } +.f3 { font-family: 'Inter' } +.f4 { font-family: inter } /* case-insensitive match in browsers */ +.f5 { font-family: Roboto Mono, monospace } +.f6 { font-family: "Roboto Mono", monospace } +.f7 { font-family: Segoe UI, Inter, Arial, sans-serif } +.f8 { font: 12px Inter } +.f9 { font: bold 12px/1 "Roboto Mono", monospace } +.f10 { font: italic small-caps bold condensed 16px/2 cursive } +.f11 { font: caption } +.f12 { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto } +.f13 { font-family: "Nonexistent Face" } +.f14 { font-family: Inter !important } +.f15 { font-family: var(--font, Inter) } +.f16 { font-family: "Inter", } +.f17 { font-family: , Inter } +.f18 { font-family: "Inter"Inter } +.f19 { font-family: Inter Inter } +.f20 { font-family: "Inter" "Inter" } diff --git a/parser/src/test-data/web/torture/css/gaps-many.css b/parser/src/test-data/web/torture/css/gaps-many.css new file mode 100644 index 00000000..c9582e34 --- /dev/null +++ b/parser/src/test-data/web/torture/css/gaps-many.css @@ -0,0 +1,300 @@ +.k0 { color: rgb(1, } +.k1 { color: rgb(1, } +.k2 { color: rgb(1, } +.k3 { color: rgb(1, } +.k4 { color: rgb(1, } +.k5 { color: rgb(1, } +.k6 { color: rgb(1, } +.k7 { color: rgb(1, } +.k8 { color: rgb(1, } +.k9 { color: rgb(1, } +.k10 { color: rgb(1, } +.k11 { color: rgb(1, } +.k12 { color: rgb(1, } +.k13 { color: rgb(1, } +.k14 { color: rgb(1, } +.k15 { color: rgb(1, } +.k16 { color: rgb(1, } +.k17 { color: rgb(1, } +.k18 { color: rgb(1, } +.k19 { color: rgb(1, } +.k20 { color: rgb(1, } +.k21 { color: rgb(1, } +.k22 { color: rgb(1, } +.k23 { color: rgb(1, } +.k24 { color: rgb(1, } +.k25 { color: rgb(1, } +.k26 { color: rgb(1, } +.k27 { color: rgb(1, } +.k28 { color: rgb(1, } +.k29 { color: rgb(1, } +.k30 { color: rgb(1, } +.k31 { color: rgb(1, } +.k32 { color: rgb(1, } +.k33 { color: rgb(1, } +.k34 { color: rgb(1, } +.k35 { color: rgb(1, } +.k36 { color: rgb(1, } +.k37 { color: rgb(1, } +.k38 { color: rgb(1, } +.k39 { color: rgb(1, } +.k40 { color: rgb(1, } +.k41 { color: rgb(1, } +.k42 { color: rgb(1, } +.k43 { color: rgb(1, } +.k44 { color: rgb(1, } +.k45 { color: rgb(1, } +.k46 { color: rgb(1, } +.k47 { color: rgb(1, } +.k48 { color: rgb(1, } +.k49 { color: rgb(1, } +.k50 { color: rgb(1, } +.k51 { color: rgb(1, } +.k52 { color: rgb(1, } +.k53 { color: rgb(1, } +.k54 { color: rgb(1, } +.k55 { color: rgb(1, } +.k56 { color: rgb(1, } +.k57 { color: rgb(1, } +.k58 { color: rgb(1, } +.k59 { color: rgb(1, } +.k60 { color: rgb(1, } +.k61 { color: rgb(1, } +.k62 { color: rgb(1, } +.k63 { color: rgb(1, } +.k64 { color: rgb(1, } +.k65 { color: rgb(1, } +.k66 { color: rgb(1, } +.k67 { color: rgb(1, } +.k68 { color: rgb(1, } +.k69 { color: rgb(1, } +.k70 { color: rgb(1, } +.k71 { color: rgb(1, } +.k72 { color: rgb(1, } +.k73 { color: rgb(1, } +.k74 { color: rgb(1, } +.k75 { color: rgb(1, } +.k76 { color: rgb(1, } +.k77 { color: rgb(1, } +.k78 { color: rgb(1, } +.k79 { color: rgb(1, } +.k80 { color: rgb(1, } +.k81 { color: rgb(1, } +.k82 { color: rgb(1, } +.k83 { color: rgb(1, } +.k84 { color: rgb(1, } +.k85 { color: rgb(1, } +.k86 { color: rgb(1, } +.k87 { color: rgb(1, } +.k88 { color: rgb(1, } +.k89 { color: rgb(1, } +.k90 { color: rgb(1, } +.k91 { color: rgb(1, } +.k92 { color: rgb(1, } +.k93 { color: rgb(1, } +.k94 { color: rgb(1, } +.k95 { color: rgb(1, } +.k96 { color: rgb(1, } +.k97 { color: rgb(1, } +.k98 { color: rgb(1, } +.k99 { color: rgb(1, } +.k100 { color: rgb(1, } +.k101 { color: rgb(1, } +.k102 { color: rgb(1, } +.k103 { color: rgb(1, } +.k104 { color: rgb(1, } +.k105 { color: rgb(1, } +.k106 { color: rgb(1, } +.k107 { color: rgb(1, } +.k108 { color: rgb(1, } +.k109 { color: rgb(1, } +.k110 { color: rgb(1, } +.k111 { color: rgb(1, } +.k112 { color: rgb(1, } +.k113 { color: rgb(1, } +.k114 { color: rgb(1, } +.k115 { color: rgb(1, } +.k116 { color: rgb(1, } +.k117 { color: rgb(1, } +.k118 { color: rgb(1, } +.k119 { color: rgb(1, } +.k120 { color: rgb(1, } +.k121 { color: rgb(1, } +.k122 { color: rgb(1, } +.k123 { color: rgb(1, } +.k124 { color: rgb(1, } +.k125 { color: rgb(1, } +.k126 { color: rgb(1, } +.k127 { color: rgb(1, } +.k128 { color: rgb(1, } +.k129 { color: rgb(1, } +.k130 { color: rgb(1, } +.k131 { color: rgb(1, } +.k132 { color: rgb(1, } +.k133 { color: rgb(1, } +.k134 { color: rgb(1, } +.k135 { color: rgb(1, } +.k136 { color: rgb(1, } +.k137 { color: rgb(1, } +.k138 { color: rgb(1, } +.k139 { color: rgb(1, } +.k140 { color: rgb(1, } +.k141 { color: rgb(1, } +.k142 { color: rgb(1, } +.k143 { color: rgb(1, } +.k144 { color: rgb(1, } +.k145 { color: rgb(1, } +.k146 { color: rgb(1, } +.k147 { color: rgb(1, } +.k148 { color: rgb(1, } +.k149 { color: rgb(1, } +.k150 { color: rgb(1, } +.k151 { color: rgb(1, } +.k152 { color: rgb(1, } +.k153 { color: rgb(1, } +.k154 { color: rgb(1, } +.k155 { color: rgb(1, } +.k156 { color: rgb(1, } +.k157 { color: rgb(1, } +.k158 { color: rgb(1, } +.k159 { color: rgb(1, } +.k160 { color: rgb(1, } +.k161 { color: rgb(1, } +.k162 { color: rgb(1, } +.k163 { color: rgb(1, } +.k164 { color: rgb(1, } +.k165 { color: rgb(1, } +.k166 { color: rgb(1, } +.k167 { color: rgb(1, } +.k168 { color: rgb(1, } +.k169 { color: rgb(1, } +.k170 { color: rgb(1, } +.k171 { color: rgb(1, } +.k172 { color: rgb(1, } +.k173 { color: rgb(1, } +.k174 { color: rgb(1, } +.k175 { color: rgb(1, } +.k176 { color: rgb(1, } +.k177 { color: rgb(1, } +.k178 { color: rgb(1, } +.k179 { color: rgb(1, } +.k180 { color: rgb(1, } +.k181 { color: rgb(1, } +.k182 { color: rgb(1, } +.k183 { color: rgb(1, } +.k184 { color: rgb(1, } +.k185 { color: rgb(1, } +.k186 { color: rgb(1, } +.k187 { color: rgb(1, } +.k188 { color: rgb(1, } +.k189 { color: rgb(1, } +.k190 { color: rgb(1, } +.k191 { color: rgb(1, } +.k192 { color: rgb(1, } +.k193 { color: rgb(1, } +.k194 { color: rgb(1, } +.k195 { color: rgb(1, } +.k196 { color: rgb(1, } +.k197 { color: rgb(1, } +.k198 { color: rgb(1, } +.k199 { color: rgb(1, } +.k200 { color: rgb(1, } +.k201 { color: rgb(1, } +.k202 { color: rgb(1, } +.k203 { color: rgb(1, } +.k204 { color: rgb(1, } +.k205 { color: rgb(1, } +.k206 { color: rgb(1, } +.k207 { color: rgb(1, } +.k208 { color: rgb(1, } +.k209 { color: rgb(1, } +.k210 { color: rgb(1, } +.k211 { color: rgb(1, } +.k212 { color: rgb(1, } +.k213 { color: rgb(1, } +.k214 { color: rgb(1, } +.k215 { color: rgb(1, } +.k216 { color: rgb(1, } +.k217 { color: rgb(1, } +.k218 { color: rgb(1, } +.k219 { color: rgb(1, } +.k220 { color: rgb(1, } +.k221 { color: rgb(1, } +.k222 { color: rgb(1, } +.k223 { color: rgb(1, } +.k224 { color: rgb(1, } +.k225 { color: rgb(1, } +.k226 { color: rgb(1, } +.k227 { color: rgb(1, } +.k228 { color: rgb(1, } +.k229 { color: rgb(1, } +.k230 { color: rgb(1, } +.k231 { color: rgb(1, } +.k232 { color: rgb(1, } +.k233 { color: rgb(1, } +.k234 { color: rgb(1, } +.k235 { color: rgb(1, } +.k236 { color: rgb(1, } +.k237 { color: rgb(1, } +.k238 { color: rgb(1, } +.k239 { color: rgb(1, } +.k240 { color: rgb(1, } +.k241 { color: rgb(1, } +.k242 { color: rgb(1, } +.k243 { color: rgb(1, } +.k244 { color: rgb(1, } +.k245 { color: rgb(1, } +.k246 { color: rgb(1, } +.k247 { color: rgb(1, } +.k248 { color: rgb(1, } +.k249 { color: rgb(1, } +.k250 { color: rgb(1, } +.k251 { color: rgb(1, } +.k252 { color: rgb(1, } +.k253 { color: rgb(1, } +.k254 { color: rgb(1, } +.k255 { color: rgb(1, } +.k256 { color: rgb(1, } +.k257 { color: rgb(1, } +.k258 { color: rgb(1, } +.k259 { color: rgb(1, } +.k260 { color: rgb(1, } +.k261 { color: rgb(1, } +.k262 { color: rgb(1, } +.k263 { color: rgb(1, } +.k264 { color: rgb(1, } +.k265 { color: rgb(1, } +.k266 { color: rgb(1, } +.k267 { color: rgb(1, } +.k268 { color: rgb(1, } +.k269 { color: rgb(1, } +.k270 { color: rgb(1, } +.k271 { color: rgb(1, } +.k272 { color: rgb(1, } +.k273 { color: rgb(1, } +.k274 { color: rgb(1, } +.k275 { color: rgb(1, } +.k276 { color: rgb(1, } +.k277 { color: rgb(1, } +.k278 { color: rgb(1, } +.k279 { color: rgb(1, } +.k280 { color: rgb(1, } +.k281 { color: rgb(1, } +.k282 { color: rgb(1, } +.k283 { color: rgb(1, } +.k284 { color: rgb(1, } +.k285 { color: rgb(1, } +.k286 { color: rgb(1, } +.k287 { color: rgb(1, } +.k288 { color: rgb(1, } +.k289 { color: rgb(1, } +.k290 { color: rgb(1, } +.k291 { color: rgb(1, } +.k292 { color: rgb(1, } +.k293 { color: rgb(1, } +.k294 { color: rgb(1, } +.k295 { color: rgb(1, } +.k296 { color: rgb(1, } +.k297 { color: rgb(1, } +.k298 { color: rgb(1, } +.k299 { color: rgb(1, } diff --git a/parser/src/test-data/web/torture/css/gaps.css b/parser/src/test-data/web/torture/css/gaps.css new file mode 100644 index 00000000..5f8559ef --- /dev/null +++ b/parser/src/test-data/web/torture/css/gaps.css @@ -0,0 +1,34 @@ +/* G01 syntax the grammar must survive, with rows still emitted for the readable rest */ +.before-gaps { color: red } +.g1 { color: red +.g2 { color: blue } +.g3 { color: red }} +} +.g4 { ; color: red } +.g5 { color: red; ; ; } +.g6 { color } +.g7 { : } +.g8 { @media (x) { color: red } } +.g9 { color: red } .g10 +.g11 { color: red } } +@media { .g12 { color: red } } +@media screen { .g13 { color: red } +.g14 { color: red } +@unknown-rule foo { .g15 { color: red } } +@unknown-statement foo bar; +@import "late.css"; /* @import after rules is ignored by browsers */ +@charset "utf-8"; /* @charset not first is ignored by browsers */ +.g16 { color: red } /* unclosed comment +.g17 { color: red } +*/ +.g18 { color: red } + +.g20 { color: red } +.g21 { background: url(img/bg.png } +.g22 { color: rgb(1, 2 } +.g23 { width: calc(1px + } +.g24 { content: "x } +.g25 { color: red } +.g26 [ { color: red } +.g27 ( { color: red } +.g28 { color: red diff --git a/parser/src/test-data/web/torture/css/imported-by-style.css b/parser/src/test-data/web/torture/css/imported-by-style.css new file mode 100644 index 00000000..09a3be8f --- /dev/null +++ b/parser/src/test-data/web/torture/css/imported-by-style.css @@ -0,0 +1,2 @@ +.imported-by-style { color: red } +@import "tokens.css"; diff --git a/parser/src/test-data/web/torture/css/layers.css b/parser/src/test-data/web/torture/css/layers.css new file mode 100644 index 00000000..91288b03 --- /dev/null +++ b/parser/src/test-data/web/torture/css/layers.css @@ -0,0 +1,17 @@ +/* L01 cascade layers: statement, block, nested, dotted, anonymous, imported-into, media-wrapped */ +@layer reset, base, components.buttons, components.cards, utilities; +@layer reset { * { margin: 0 } } +@layer base { .card { color: red } @layer typography { h1 { font-size: 2rem } } } +@layer components { @layer buttons { .btn { color: red } } @layer cards { .card { color: blue } } } +@layer components.cards { .card .item { color: green } } +@layer base.typography { h2 { font-size: 1rem } } +@layer { .anonymous { color: red } } +@media screen { @layer utilities { .u-hide { display: none } } } +@layer utilities { @media print { .u-hide { display: block } } } +@import "print.css" layer(print-layer); +@import "alt.css" layer; +@layer sub.sub.sub { .deep-layer { color: red } } +@layer a, b, a; +@layer reset; +@layer spaced , names ; +.unlayered { color: red } diff --git a/parser/src/test-data/web/torture/css/long-line.css b/parser/src/test-data/web/torture/css/long-line.css new file mode 100644 index 00000000..af1eb44d --- /dev/null +++ b/parser/src/test-data/web/torture/css/long-line.css @@ -0,0 +1 @@ +.m1{color:red}.m2{color:red}.m3{color:red}.m4{color:red}.m5{color:red}.m6{color:red}.m7{color:red}.m8{color:red}.m9{color:red}.m10{color:red}.m11{color:red}.m12{color:red}.m13{color:red}.m14{color:red}.m15{color:red}.m16{color:red}.m17{color:red}.m18{color:red}.m19{color:red}.m20{color:red}.m21{color:red}.m22{color:red}.m23{color:red}.m24{color:red}.m25{color:red}.m26{color:red}.m27{color:red}.m28{color:red}.m29{color:red}.m30{color:red}.m31{color:red}.m32{color:red}.m33{color:red}.m34{color:red}.m35{color:red}.m36{color:red}.m37{color:red}.m38{color:red}.m39{color:red}.m40{color:red}.m41{color:red}.m42{color:red}.m43{color:red}.m44{color:red}.m45{color:red}.m46{color:red}.m47{color:red}.m48{color:red}.m49{color:red}.m50{color:red}.m51{color:red}.m52{color:red}.m53{color:red}.m54{color:red}.m55{color:red}.m56{color:red}.m57{color:red}.m58{color:red}.m59{color:red}.m60{color:red}.m61{color:red}.m62{color:red}.m63{color:red}.m64{color:red}.m65{color:red}.m66{color:red}.m67{color:red}.m68{color:red}.m69{color:red}.m70{color:red}.m71{color:red}.m72{color:red}.m73{color:red}.m74{color:red}.m75{color:red}.m76{color:red}.m77{color:red}.m78{color:red}.m79{color:red}.m80{color:red}.m81{color:red}.m82{color:red}.m83{color:red}.m84{color:red}.m85{color:red}.m86{color:red}.m87{color:red}.m88{color:red}.m89{color:red}.m90{color:red}.m91{color:red}.m92{color:red}.m93{color:red}.m94{color:red}.m95{color:red}.m96{color:red}.m97{color:red}.m98{color:red}.m99{color:red}.m100{color:red}.m101{color:red}.m102{color:red}.m103{color:red}.m104{color:red}.m105{color:red}.m106{color:red}.m107{color:red}.m108{color:red}.m109{color:red}.m110{color:red}.m111{color:red}.m112{color:red}.m113{color:red}.m114{color:red}.m115{color:red}.m116{color:red}.m117{color:red}.m118{color:red}.m119{color:red}.m120{color:red}.m121{color:red}.m122{color:red}.m123{color:red}.m124{color:red}.m125{color:red}.m126{color:red}.m127{color:red}.m128{color:red}.m129{color:red}.m130{color:red}.m131{color:red}.m132{color:red}.m133{color:red}.m134{color:red}.m135{color:red}.m136{color:red}.m137{color:red}.m138{color:red}.m139{color:red}.m140{color:red}.m141{color:red}.m142{color:red}.m143{color:red}.m144{color:red}.m145{color:red}.m146{color:red}.m147{color:red}.m148{color:red}.m149{color:red}.m150{color:red}.m151{color:red}.m152{color:red}.m153{color:red}.m154{color:red}.m155{color:red}.m156{color:red}.m157{color:red}.m158{color:red}.m159{color:red}.m160{color:red}.m161{color:red}.m162{color:red}.m163{color:red}.m164{color:red}.m165{color:red}.m166{color:red}.m167{color:red}.m168{color:red}.m169{color:red}.m170{color:red}.m171{color:red}.m172{color:red}.m173{color:red}.m174{color:red}.m175{color:red}.m176{color:red}.m177{color:red}.m178{color:red}.m179{color:red}.m180{color:red}.m181{color:red}.m182{color:red}.m183{color:red}.m184{color:red}.m185{color:red}.m186{color:red}.m187{color:red}.m188{color:red}.m189{color:red}.m190{color:red}.m191{color:red}.m192{color:red}.m193{color:red}.m194{color:red}.m195{color:red}.m196{color:red}.m197{color:red}.m198{color:red}.m199{color:red}.m200{color:red}.m201{color:red}.m202{color:red}.m203{color:red}.m204{color:red}.m205{color:red}.m206{color:red}.m207{color:red}.m208{color:red}.m209{color:red}.m210{color:red}.m211{color:red}.m212{color:red}.m213{color:red}.m214{color:red}.m215{color:red}.m216{color:red}.m217{color:red}.m218{color:red}.m219{color:red}.m220{color:red}.m221{color:red}.m222{color:red}.m223{color:red}.m224{color:red}.m225{color:red}.m226{color:red}.m227{color:red}.m228{color:red}.m229{color:red}.m230{color:red}.m231{color:red}.m232{color:red}.m233{color:red}.m234{color:red}.m235{color:red}.m236{color:red}.m237{color:red}.m238{color:red}.m239{color:red}.m240{color:red}.m241{color:red}.m242{color:red}.m243{color:red}.m244{color:red}.m245{color:red}.m246{color:red}.m247{color:red}.m248{color:red}.m249{color:red}.m250{color:red}.m251{color:red}.m252{color:red}.m253{color:red}.m254{color:red}.m255{color:red}.m256{color:red}.m257{color:red}.m258{color:red}.m259{color:red}.m260{color:red}.m261{color:red}.m262{color:red}.m263{color:red}.m264{color:red}.m265{color:red}.m266{color:red}.m267{color:red}.m268{color:red}.m269{color:red}.m270{color:red}.m271{color:red}.m272{color:red}.m273{color:red}.m274{color:red}.m275{color:red}.m276{color:red}.m277{color:red}.m278{color:red}.m279{color:red}.m280{color:red}.m281{color:red}.m282{color:red}.m283{color:red}.m284{color:red}.m285{color:red}.m286{color:red}.m287{color:red}.m288{color:red}.m289{color:red}.m290{color:red}.m291{color:red}.m292{color:red}.m293{color:red}.m294{color:red}.m295{color:red}.m296{color:red}.m297{color:red}.m298{color:red}.m299{color:red}.m300{color:red}.m301{color:red}.m302{color:red}.m303{color:red}.m304{color:red}.m305{color:red}.m306{color:red}.m307{color:red}.m308{color:red}.m309{color:red}.m310{color:red}.m311{color:red}.m312{color:red}.m313{color:red}.m314{color:red}.m315{color:red}.m316{color:red}.m317{color:red}.m318{color:red}.m319{color:red}.m320{color:red}.m321{color:red}.m322{color:red}.m323{color:red}.m324{color:red}.m325{color:red}.m326{color:red}.m327{color:red}.m328{color:red}.m329{color:red}.m330{color:red}.m331{color:red}.m332{color:red}.m333{color:red}.m334{color:red}.m335{color:red}.m336{color:red}.m337{color:red}.m338{color:red}.m339{color:red}.m340{color:red}.m341{color:red}.m342{color:red}.m343{color:red}.m344{color:red}.m345{color:red}.m346{color:red}.m347{color:red}.m348{color:red}.m349{color:red}.m350{color:red}.m351{color:red}.m352{color:red}.m353{color:red}.m354{color:red}.m355{color:red}.m356{color:red}.m357{color:red}.m358{color:red}.m359{color:red}.m360{color:red}.m361{color:red}.m362{color:red}.m363{color:red}.m364{color:red}.m365{color:red}.m366{color:red}.m367{color:red}.m368{color:red}.m369{color:red}.m370{color:red}.m371{color:red}.m372{color:red}.m373{color:red}.m374{color:red}.m375{color:red}.m376{color:red}.m377{color:red}.m378{color:red}.m379{color:red}.m380{color:red}.m381{color:red}.m382{color:red}.m383{color:red}.m384{color:red}.m385{color:red}.m386{color:red}.m387{color:red}.m388{color:red}.m389{color:red}.m390{color:red}.m391{color:red}.m392{color:red}.m393{color:red}.m394{color:red}.m395{color:red}.m396{color:red}.m397{color:red}.m398{color:red}.m399{color:red}.m400{color:red} \ No newline at end of file diff --git a/parser/src/test-data/web/torture/css/nested.css b/parser/src/test-data/web/torture/css/nested.css new file mode 100644 index 00000000..8832cf59 --- /dev/null +++ b/parser/src/test-data/web/torture/css/nested.css @@ -0,0 +1,45 @@ +/* N01 CSS nesting: every & position, implicit nesting, nested at-rules with bare declarations */ +.card { + color: red; + &:hover { color: blue } + & .item { color: green } + .item & { color: yellow } + & + & { margin: 0 } + &.is-active { outline: 1px } + &#section-one { outline: 2px } + & > .nav, & ~ .nav { color: red } + .item, .nav { color: red } /* implicit & descendant, two selectors */ + > .direct { color: red } /* leading combinator */ + + .sib { color: red } + ~ .gen { color: red } + :hover { color: red } /* implicit: &:hover? no, & :hover descendant */ + .a &, .b & { color: red } + &, &.x { color: red } + :is(&, .alias) .item { color: red } + &:not(.is-active) .item { color: red } + .parent & .child { color: red } + && { color: red } + & & { color: red } + @media (min-width: 1px) { color: red; & .item { color: blue } .nested-in-media { color: green } } + @supports (display: grid) { display: grid } + @container sidebar (min-width: 1px) { color: red } + @layer nested-layer { color: red } + @scope (.card) { .item { color: red } } + @starting-style { opacity: 0 } + --nested-var: 1; + .deep { .deeper { .deepest { color: var(--nested-var) } } } + @media (a) { @media (b) { @supports (c) { .x { color: red } } } } + & .item { & .leaf { color: red } } + .item { .leaf { color: red } } + @nest .parent & { color: red } /* pre-standard @nest */ + color: blue; /* declaration after nested rules */ +} +.card.is-active { & .item { color: red } } +.a, .b { & .c { color: red } .d & { color: red } } +@media (x) { .outer { .inner { color: red } } } +@layer l { .outer { & .inner { color: red } } } +.p { &__elem { color: red } &--mod { color: red } } /* BEM-style: invalid in CSS nesting, valid in Sass */ +.q { &-suffix { color: red } } +.r { &:is(.s) { color: red } } +.t { .u & .v & { color: red } } +.w { &.x.y > &.z { color: red } } diff --git a/parser/src/test-data/web/torture/css/norel.css b/parser/src/test-data/web/torture/css/norel.css new file mode 100644 index 00000000..867918fd --- /dev/null +++ b/parser/src/test-data/web/torture/css/norel.css @@ -0,0 +1 @@ +.norel-only { color: red } diff --git a/parser/src/test-data/web/torture/css/print.css b/parser/src/test-data/web/torture/css/print.css new file mode 100644 index 00000000..8c1f9cba --- /dev/null +++ b/parser/src/test-data/web/torture/css/print.css @@ -0,0 +1,2 @@ +@media print { .print-rule { color: red } } +.print-only { color: red } diff --git a/parser/src/test-data/web/torture/css/sassy.css b/parser/src/test-data/web/torture/css/sassy.css new file mode 100644 index 00000000..8bb5165e --- /dev/null +++ b/parser/src/test-data/web/torture/css/sassy.css @@ -0,0 +1,5 @@ +$primary: red; +@mixin m { color: $primary } +.sassy { @include m; &__elem { color: red } } +// line comment +.x { .y { color: red } } diff --git a/parser/src/test-data/web/torture/css/selectors.css b/parser/src/test-data/web/torture/css/selectors.css new file mode 100644 index 00000000..d1bf6329 --- /dev/null +++ b/parser/src/test-data/web/torture/css/selectors.css @@ -0,0 +1,34 @@ +/* E01 escaped identifiers: every form the CSS syntax admits */ +.md\:flex { display: flex } +.md\3A flex { display: flex } +.md\00003Aflex { display: flex } +.w-1\/2 { width: 50% } +.\31 23 { order: 1 } +.\31 0 { order: 10 } +.\-mt-2 { margin: 0 } +.a\ b { color: red } +.\@media { color: red } +.\#hash { color: red } +.\.dot { color: red } +.\\backslash { color: red } +.caf\e9 { color: red } +.café { color: red } +.\1F680 { color: red } +.🚀 { color: red } +#\31 23 { color: red } +#id\:colon { color: red } +[data-x\:y] { color: red } +[data-x="a\"b"] { color: red } +svg|rect\:x { color: red } +.a\,b { color: red } +.a\>b { color: red } +.a\&b { color: red } +.\[\&_svg\]\:size-4 svg { color: red } +.\!font-bold { color: red } +.\32 xl\:grid-cols-\[repeat\(auto-fill\2c minmax\(200px\2c 1fr\)\)\] { color: red } +.group:hover .group-hover\:visible { color: red } +.peer:checked ~ .peer-checked\:block { color: red } +.data-\[state\=open\]\:bg-red[data-state="open"] { color: red } +.aria-\[checked\]\:bg-red[aria-checked] { color: red } +.supports-\[display\:grid\]\:grid { color: red } +.has-\[\>img\]\:p-0:has(> img) { color: red } diff --git a/parser/src/test-data/web/torture/css/strings.css b/parser/src/test-data/web/torture/css/strings.css new file mode 100644 index 00000000..a87c6197 --- /dev/null +++ b/parser/src/test-data/web/torture/css/strings.css @@ -0,0 +1,36 @@ +/* S01 strings and comments that contain CSS syntax */ +.s1 { content: "}" } +.s2 { content: "{" } +.s3 { content: ";" } +.s4 { content: "a; color: red" } +.s5 { content: "/* not a comment */" } +.s6 { content: "url(not.png)" } +.s7 { content: 'var(--not)' } +.s8 { content: "\"" } +.s9 { content: '\'' } +.s10 { content: "\201C \201D" } +.s11 { content: "line\ +continued" } +.s12 { content: "unterminated } +.s13 { color: red } +.s14 { content: "\"; } .injected { color: red }" } +.s15 { background: url("a)b.png") } +.s16 { background: url('a"b.png') } +.s17 { background: url(a\)b.png) } +.s18 { font-family: "Font; Name" } +.s19 { font-family: "Font } Name" } +.s20 { quotes: "«" "»" } +.s21 { content: attr(data-page) } +.s22 { content: counter(a) " / " counter(b) } +.s23 { grid-template-areas: "header header" "sidebar main" } +.s24 { content: "{{ not_a_template }}" } +.s25 { content: "<%= not_erb %>" } +.s26 { content: "${not_dollar}" } +.s27[data-x="}"] { color: red } +.s28[data-x=";"] { color: red } +.s29[data-x="url(x)"] { color: red } +.s30:not([data-x="{"]) { color: red } +/* .s31 { color: red } */ +/* a comment with url(img/bg.png) and var(--x) and .commented-class */ +/* unterminated comment at the end of the file +.s32 { color: red } diff --git a/parser/src/test-data/web/torture/css/theme.css b/parser/src/test-data/web/torture/css/theme.css new file mode 100644 index 00000000..f6f83199 --- /dev/null +++ b/parser/src/test-data/web/torture/css/theme.css @@ -0,0 +1,14 @@ +/* K02 the end of the import chain: app -> tokens -> theme; imports tokens back (cycle) */ +@import "tokens.css"; +.theme-light { --brand: #0af } +.theme-dark { --brand: #a0f } +@keyframes fade { from { opacity: 0 } to { opacity: 1 } } +@keyframes spin { from { transform: rotate(0) } 50% { opacity: .5 } to { transform: rotate(1turn) } } +@keyframes spin { to { transform: rotate(2turn) } } /* redefinition: the last one wins */ +@keyframes "quoted-name" { to { opacity: 0 } } +@keyframes ease { to { opacity: 0 } } /* a keyframes named like a keyword */ +@keyframes none { to { opacity: 0 } } /* invalid name, written anyway */ +@keyframes slide { 0%, 50% { left: 0 } 100% { left: 1px } 33.3% { left: 2px } from, to { left: 3px } } +@keyframes var-inside { to { color: var(--brand); background: url(img/bg.png) } } +@-webkit-keyframes spin { to { transform: rotate(0) } } +@-moz-keyframes spin { to { transform: rotate(0) } } diff --git a/parser/src/test-data/web/torture/css/tokens.css b/parser/src/test-data/web/torture/css/tokens.css new file mode 100644 index 00000000..7be1084f --- /dev/null +++ b/parser/src/test-data/web/torture/css/tokens.css @@ -0,0 +1,35 @@ +/* K01 custom properties: defined at :root, overridden per theme, registered, used in fallbacks and shorthands */ +@import "theme.css"; +:root { + --brand: #09f; + --gap: 8px; + --anchor: var(--brand); + --fallback-brand: pink; + --font: Inter, sans-serif; + --anim: spin; + --duration: 1s; + --easing: ease-in; + --pad: 1px; + --wide: 70rem; +} +:root.dark, .dark, [data-theme="dark"], html[data-theme=dark] body { --brand: #fff; } +@media (prefers-color-scheme: dark) { :root { --brand: #000 } } +@property --brand { syntax: ''; inherits: true; initial-value: red } +@property --registered-never-used { syntax: '*'; inherits: false } +.scoped { --gap: 4px; } +.scoped .child { gap: var(--gap); } +.uses-undefined { color: var(--undefined-anywhere); } +.uses-undefined-with-fallback { color: var(--undefined-anywhere, red); } +.defined-in-style-attr { padding: var(--local); } /* --local is defined only in a style="" attribute of index.html */ +.var-in-selector-arg:is(.x) { --in-is: 1 } +.var-name-cases { color: var(--BRAND); color: var( --brand ); color: var(--brand,); color: var(--brand , 1px 2px); color: var(--brand, url(img/bg.png)); color: var(--brand, var(--gap)) } +.var-not-a-var { color: var(brand); color: var(); color: var(--); } +.var-in-shorthand { margin: var(--gap) calc(var(--gap) * 2) 0 var(--gap, var(--pad)); } +.var-in-important { color: var(--brand) !important; } +.var-in-url { background: url(var(--url)); } /* not valid CSS, but written */ +.var-in-string { content: "var(--brand)"; } +.var-in-comment { /* color: var(--commented) */ color: red; } +.var-in-custom { --alias: var(--brand); --chain: var(--alias); color: var(--chain); } +.var-in-animation { animation: var(--anim) var(--duration); } +.var-in-font { font-family: var(--font); font: 12px var(--font); } +.var-in-container { container-name: var(--container-name); } diff --git a/parser/src/test-data/web/torture/css/upper.css b/parser/src/test-data/web/torture/css/upper.css new file mode 100644 index 00000000..f31c0364 --- /dev/null +++ b/parser/src/test-data/web/torture/css/upper.css @@ -0,0 +1 @@ +.upper-only { color: red } diff --git a/parser/src/test-data/web/torture/css/vendor.min.css b/parser/src/test-data/web/torture/css/vendor.min.css new file mode 100644 index 00000000..154c5ba5 --- /dev/null +++ b/parser/src/test-data/web/torture/css/vendor.min.css @@ -0,0 +1 @@ +.vendor{color:red}.card{color:rgb(200,200,200)} \ No newline at end of file diff --git a/parser/src/test-data/web/torture/fonts/inter.woff b/parser/src/test-data/web/torture/fonts/inter.woff new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/fonts/inter.woff2 b/parser/src/test-data/web/torture/fonts/inter.woff2 new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/forms.html b/parser/src/test-data/web/torture/forms.html new file mode 100644 index 00000000..b95f4594 --- /dev/null +++ b/parser/src/test-data/web/torture/forms.html @@ -0,0 +1,51 @@ + + + + Torture: id references + + + + + + + + +
+ + + + + + + + +
nameage
x
+
+ + +
pop
+ + + + legacy named anchor + to a name, not an id + to an id + case-different fragment + bare hash + implicit top + to the other page + to an uppercase id on the other page + cross-page fragment, dangling + cross-page fragment, page missing + + targets frame-b + targets a keyword +
+
+
+
+
+ + duplicate id with the first input + + diff --git a/parser/src/test-data/web/torture/img/bg.png b/parser/src/test-data/web/torture/img/bg.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/cursor.cur b/parser/src/test-data/web/torture/img/cursor.cur new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/favicon.ico b/parser/src/test-data/web/torture/img/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/hero.png b/parser/src/test-data/web/torture/img/hero.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/hero.webp b/parser/src/test-data/web/torture/img/hero.webp new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/logo.png b/parser/src/test-data/web/torture/img/logo.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/logo@2x.png b/parser/src/test-data/web/torture/img/logo@2x.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/space name.png b/parser/src/test-data/web/torture/img/space name.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/img/sprite.svg b/parser/src/test-data/web/torture/img/sprite.svg new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/index.html b/parser/src/test-data/web/torture/index.html new file mode 100644 index 00000000..6dd32a5a --- /dev/null +++ b/parser/src/test-data/web/torture/index.html @@ -0,0 +1,208 @@ + + + + + Torture: the main page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
whitespace, tab entity, newline, duplicate token, padded id
+
valueless class attribute
+
empty class attribute
+
entity-bearing class tokens
+
case variants: CSS .upper must NOT match Upper
+
uppercase tag and attribute names
+
duplicate class attribute: first wins, second is a gap
+ + + + +
+

duplicate id on the page

+ +

var in style attribute

+

custom property defined in a style attribute

+

urls in style attribute, relative to the page

+

root-relative url in style attribute

+

important and font shorthand

+

template in style attribute

+

broken declaration list, second declaration must survive

+

a rule where a declaration list belongs

+

empty style

+

keyframes, font, container refs in a style attribute

+

brace in a style attribute

+

uppercase property names

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; trailingText(); + + + + comma inside a srcset url + + + + + + + + + + + + +
cite
+ + + + + + +
+ a browser makes me a CHILD of the div above +

a

div inside a p closes the p
trailing

+
  • one
  • two
+ +
no tbody written
+
c
h
body row after thead
+ outer nested anchor +
t
d
t2
+
+ + + +
+ + +

entities:   & <tag> ' ' &unknown; ©

+

aria idrefs

+ +
styled only by base.css, which this page does not load (its link is under the base)
+
+ + + diff --git a/parser/src/test-data/web/torture/js/app.js b/parser/src/test-data/web/torture/js/app.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/js/lib.js b/parser/src/test-data/web/torture/js/lib.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/js/module.js b/parser/src/test-data/web/torture/js/module.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/js/vendor.js b/parser/src/test-data/web/torture/js/vendor.js new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/media/clip.mp4 b/parser/src/test-data/web/torture/media/clip.mp4 new file mode 100644 index 00000000..93db0574 --- /dev/null +++ b/parser/src/test-data/web/torture/media/clip.mp4 @@ -0,0 +1 @@ +a{color:red} \ No newline at end of file diff --git a/parser/src/test-data/web/torture/media/clip.vtt b/parser/src/test-data/web/torture/media/clip.vtt new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/media/poster.png b/parser/src/test-data/web/torture/media/poster.png new file mode 100644 index 00000000..e69de29b diff --git a/parser/src/test-data/web/torture/public/css/dup.css b/parser/src/test-data/web/torture/public/css/dup.css new file mode 100644 index 00000000..9a7bd6df --- /dev/null +++ b/parser/src/test-data/web/torture/public/css/dup.css @@ -0,0 +1 @@ +body{margin:0} diff --git a/parser/src/test-data/web/torture/quirks.html b/parser/src/test-data/web/torture/quirks.html new file mode 100644 index 00000000..8b505279 --- /dev/null +++ b/parser/src/test-data/web/torture/quirks.html @@ -0,0 +1,265 @@ + + +Quirks + + + +
+

crlf

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+

+

+ + \ No newline at end of file diff --git a/parser/src/test-data/web/torture/shadow/shadow.html b/parser/src/test-data/web/torture/shadow/shadow.html new file mode 100644 index 00000000..72f1a5d9 --- /dev/null +++ b/parser/src/test-data/web/torture/shadow/shadow.html @@ -0,0 +1,70 @@ + + + + Torture: scoping boundaries + + + + +
outside any shadow root
+ + + + + slotted from the light DOM + default slot + + + + + + + + + + + + + +
zero levels in: no rule reaches me
+ + +
+ + + diff --git a/parser/src/test-data/web/torture/static/css/dup.css b/parser/src/test-data/web/torture/static/css/dup.css new file mode 100644 index 00000000..fb939513 --- /dev/null +++ b/parser/src/test-data/web/torture/static/css/dup.css @@ -0,0 +1 @@ +body{margin:1px} diff --git a/parser/src/test-data/web/torture/svg.html b/parser/src/test-data/web/torture/svg.html new file mode 100644 index 00000000..5cec2740 --- /dev/null +++ b/parser/src/test-data/web/torture/svg.html @@ -0,0 +1,43 @@ + + + + Torture: foreign content + + + + + + + + + + + + + + + svg anchor + + +
html again: tag and attribute names lowercase here
+ +
+ svg title must not become the document title + + + +
+ x
+ + + diff --git a/parser/src/test-data/web/torture/templates/jinja.html b/parser/src/test-data/web/torture/templates/jinja.html new file mode 100644 index 00000000..08b80ea0 --- /dev/null +++ b/parser/src/test-data/web/torture/templates/jinja.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block head %} + + + +{% endblock %} +{% block body %} +
+ {% for item in items %} + {{ item.name }} + {% endfor %} + conditional class + +

style with template value

+

custom property from the template

+ {# a jinja comment with
#} + +
+ +{% endblock %} diff --git a/parser/src/test-data/web/torture/templates/mixed.html b/parser/src/test-data/web/torture/templates/mixed.html new file mode 100644 index 00000000..0d7eb298 --- /dev/null +++ b/parser/src/test-data/web/torture/templates/mixed.html @@ -0,0 +1,19 @@ + + + + + + +
x
+
thymeleaf inline
+
angular
+
vue
+
alpine
+
htmx
+
livewire, liveview, hyperscript, knockout
+
<%= body %>
+
+
    @foreach (var i in Model.Items) {
  • @i.Name
  • }
+

${name} and #{expr} and @{link} and {#block}

+ + diff --git a/parser/src/test-data/web/torture/templates/tailwind.html b/parser/src/test-data/web/torture/templates/tailwind.html new file mode 100644 index 00000000..6fe4638b --- /dev/null +++ b/parser/src/test-data/web/torture/templates/tailwind.html @@ -0,0 +1,13 @@ +
+ utility classes whose CSS selectors are escaped + stacked variants +
+ diff --git a/parser/src/test-data/web/torture/xhtml/page.xhtml b/parser/src/test-data/web/torture/xhtml/page.xhtml new file mode 100644 index 00000000..37a76c3d --- /dev/null +++ b/parser/src/test-data/web/torture/xhtml/page.xhtml @@ -0,0 +1,21 @@ + + + + + Torture: XHTML + + " } + //]]> + + + + +
+
+
+ a sibling in XML, a child in HTML + + + diff --git a/parser/src/test/discovery-tests.ts b/parser/src/test/discovery-tests.ts index e061a957..af8ea224 100644 --- a/parser/src/test/discovery-tests.ts +++ b/parser/src/test/discovery-tests.ts @@ -285,6 +285,71 @@ const CHECKS: Check[] = [ return null; }, }, + { + name: 'a-test-the-build-tsconfig-excludes-is-analysed-with-the-program-it-imports', + proves: 'a spec file and its helper, excluded by the root tsconfig (`exclude: ["test/**"]`) and ' + + 'claimed by no other config, are analysed because they import the program; an unclaimed ' + + 'file that imports nothing of the program is still reported as NO_PROGRAM_CLAIMS_FILE', + rulesOut: 'analysing only what a config claims plus what claimed files import: a build config ' + + 'that leaves its tests to the runner made every test file an orphan, so test selection ' + + 'reached none of them', + run: async (tmp) => { + const root = build(tmp, 'excluded-tests', { + 'tsconfig.json': '{"compilerOptions":{"rootDir":"./src","strict":true},"include":["src"],"exclude":["test/**"]}', + 'src/lib.ts': 'export function area(w: number, h: number): number { return w * h; }\n', + 'test/helper.ts': 'import { area } from "../src/lib";\nexport const unit = () => area(1, 1);\n', + 'test/lib.spec.ts': 'import { area } from "../src/lib";\nimport { unit } from "./helper";\nexport const ok = area(2, 3) === 6 && unit() === 1;\n', + 'tools/standalone.ts': 'export function alone(): number { return 1; }\n', + }); + const out = path.join(tmp, 'excluded-tests-out'); + const silence = console.log; + console.log = () => {}; + try { + await extractProject({ projectPath: root, versionLink: 'v1', outputDir: out, layout: 'per-language' }); + } finally { + console.log = silence; + } + const modules = fs.readFileSync(path.join(out, 'typescript', 'all-typescript-modules.csv'), 'utf-8'); + const skipped = fs.readFileSync(path.join(out, 'typescript', 'skipped-typescript-files.csv'), 'utf-8'); + if (!modules.includes('src/lib.ts')) return 'control: the claimed source file src/lib.ts is not analysed'; + const missing = ['test/lib.spec.ts', 'test/helper.ts'].filter((f) => !modules.includes(f)); + if (missing.length > 0) return `not analysed though they import the program: ${missing.join(', ')}`; + if (modules.includes('tools/standalone.ts')) return 'tools/standalone.ts imports nothing of the program, yet joined it'; + if (!/tools\/standalone\.ts\t[^\n]*NO_PROGRAM_CLAIMS_FILE/.test(skipped)) return 'tools/standalone.ts is no longer reported as NO_PROGRAM_CLAIMS_FILE'; + return null; + }, + }, + { + name: 'a-javascript-test-of-typescript-source-joins-the-typescript-program', + proves: 'a .js test that imports the TypeScript source (ESM import, and require) is analysed in the ' + + 'TypeScript program, so its calls land in the graph that holds the source; a .js file that ' + + 'imports nothing of the program is not', + rulesOut: 'leaving every .js test of .ts source to the JavaScript graph, which holds no edge into the ' + + 'TypeScript one: test selection named none of the tests that break when the source does', + run: async (tmp) => { + const root = build(tmp, 'js-tests-of-ts', { + 'tsconfig.json': '{"compilerOptions":{"strict":true},"include":["src"]}', + 'src/lib.ts': 'export function area(w: number, h: number): number { return w * h; }\n', + '__tests__/lib.js': 'import { area } from "../src/lib";\nexport const ok = area(2, 3) === 6;\n', + '__tests__/req.js': 'const lib = require("../src/lib");\nmodule.exports = lib.area(1, 1);\n', + 'scripts/alone.js': 'module.exports = function alone() { return 1; };\n', + }); + const out = path.join(tmp, 'js-tests-of-ts-out'); + const silence = console.log; + console.log = () => {}; + try { + await extractProject({ projectPath: root, versionLink: 'v1', outputDir: out, layout: 'per-language' }); + } finally { + console.log = silence; + } + const modules = fs.readFileSync(path.join(out, 'typescript', 'all-typescript-modules.csv'), 'utf-8'); + if (!modules.includes('src/lib.ts')) return 'control: the claimed source file src/lib.ts is not analysed'; + const missing = ['__tests__/lib.js', '__tests__/req.js'].filter((f) => !modules.includes(f)); + if (missing.length > 0) return `not in the TypeScript program though they import it: ${missing.join(', ')}`; + if (modules.includes('scripts/alone.js')) return 'scripts/alone.js imports nothing of the program, yet joined it'; + return null; + }, + }, { name: 'per-language-keeps-the-javascript-of-a-dist-shipping-package', proves: 'extractProject in per-language layout writes javascript/ for a package whose only ' diff --git a/parser/src/test/web-tests.ts b/parser/src/test/web-tests.ts new file mode 100644 index 00000000..316994ff --- /dev/null +++ b/parser/src/test/web-tests.ts @@ -0,0 +1,1099 @@ +/** + * WEB (HTML + CSS) TESTS — one file, mirroring services-tests.ts. + * + * npx tsx src/test/web-tests.ts # everything + * npx tsx src/test/web-tests.ts --list # what runs, and what it proves + * npx tsx src/test/web-tests.ts --bless # rewrite the goldens + * + * NO BROWSER, NO NETWORK. Every check runs the parser and compares the result + * with expectations in this file or checked into src/test-data/web. + * + * ## What this suite can and cannot do + * + * The golden is drift detection: it was produced by this parser and agrees with + * whatever the parser currently does, mistakes included. + * + * The format checks are the part that can find a defect. Each is written from + * the HTML or CSS specification (the parsing algorithm, the Selectors specificity + * rules, the URL syntax) rather than from parser output, and each names the + * plausible wrong implementation it rules out — a reader that splits `class` on + * spaces only, a specificity that counts `:where()`, a resolver that follows + * `../..` out of the project. A check no plausible implementation fails proves + * nothing, so the `rules out` line is part of the check. + * + * The structural checks — primary-key uniqueness, foreign-key integrity across + * the sixteen relations, arity against the frozen schema, enum domains, and + * determinism across two runs — are the ones that catch what a fixture cannot + * foresee. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { CssStylesheet } from '@/analysis-types/css/CssStylesheet'; +import { WEB_CSV_FILES, WEB_PARSE_GAP_LIMIT } from '@/constants/web-constants'; +import { CssSourceProvenance, CssStylesheetSource } from '@/enums/css/CssStylesheetSource'; +import { CssParseGapKind } from '@/enums/css/CssParseGapKind'; +import { CssSelectorPartKind, CssCombinator } from '@/enums/css/CssSelectorPartKind'; +import { CssValueReferenceKind } from '@/enums/css/CssValueReferenceKind'; +import { HtmlAttributeKind } from '@/enums/html/HtmlAttributeKind'; +import { HtmlDocumentKind } from '@/enums/html/HtmlDocumentKind'; +import { HtmlHandlerSource } from '@/enums/html/HtmlHandlerSource'; +import { HtmlNamespace } from '@/enums/html/HtmlNamespace'; +import { HtmlParseGapKind } from '@/enums/html/HtmlParseGapKind'; +import { HtmlScriptKind, HtmlScriptType } from '@/enums/html/HtmlScriptKind'; +import { HtmlTemplateDialect } from '@/enums/html/HtmlTemplateDialect'; +import { WebUrlKind } from '@/enums/web/WebUrlKind'; +import * as CssEnums from '@/enums/css'; +import * as HtmlEnums from '@/enums/html'; +import * as WebEnums from '@/enums/web'; +import { CssExtraction, CssParser } from '@/parsers/css/css-parser'; +import { HtmlExtraction, HtmlParser } from '@/parsers/html/html-parser'; +import { WebProjectAnalyzer } from '@/workflows/web/web-project-analyzer'; + +const DATA = 'src/test-data/web'; +const GOLDEN = path.join(DATA, '_golden'); +const SCHEMA = JSON.parse(fs.readFileSync('src/schema/web/schema.json', 'utf-8')) as { + relations: Record; commaSets?: string[] }>; +}; + +/** The values a cell holds: one, or every member of a comma-set column. */ +function cellValues(relation: string, column: string, cell: string): string[] { + if (cell === '') return []; + return (SCHEMA.relations[relation]?.commaSets ?? []).includes(column) ? cell.split(',') : [cell]; +} +const BLESS = process.argv.includes('--bless'); + +/** Columns that cannot be compared across machines: absolute paths. */ +const VOLATILE = /^(baseMservPath|filePath|resolvedFilePath)$/; +const HASH_COLUMN = /(Hash|hash)$/; + +type Row = Record; + +const fail = (m: string): number => { console.log(' ✗ ' + m); return 1; }; + +// --------------------------------------------------------------------------- +// format checks — from the specifications, in memory, no fixture tree +// --------------------------------------------------------------------------- + +/** A throwaway directory for checks that need real files to resolve against. */ +const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'axiom-web-')); +function file(rel: string, content = ''): string { + const p = path.join(TMP, rel); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, content); + return p; +} + +function parseHtml(content: string, rel = 'site/page.html'): HtmlExtraction { + const p = file(rel, content); + return new HtmlParser().parse(content, p, path.join(TMP, 'site'), 'WEB_FIXTURE_VERSION'); +} + +function parseCss(content: string, rel = 'site/css/x.css', origin = { line: 1, column: 1 }): CssExtraction & { sheet: CssStylesheet } { + const p = file(rel, content); + const sheet = new CssStylesheet({ + name: 'x', fileName: path.basename(p), filePath: p, baseMservPath: path.join(TMP, 'site'), relativePath: rel, + sourceKind: CssStylesheetSource.FILE, sourceProvenance: CssSourceProvenance.PROJECT, ownerHtmlElementLinkHash: '', + htmlDocumentLinkHash: '', startLine: 1, startColumn: 1, endLine: 1, serviceVersionLinkHash: 'WEB_FIXTURE_VERSION', + }); + const x = new CssParser().parseStylesheet(content, { + stylesheet: sheet, line: origin.line, column: origin.column, filePath: p, projectRoot: path.join(TMP, 'site'), + serviceVersionLinkHash: 'WEB_FIXTURE_VERSION', + }); + return { ...x, sheet }; +} + +/** (a,b,c) of the first selector of the first rule. */ +function specificity(selector: string): string { + const s = parseCss(`${selector} { color: red }`).selectors[0]!; + return `${s.specificityA},${s.specificityB},${s.specificityC}`; +} + +function col(text: string, needle: string, occurrence = 1): { line: number; column: number } { + let from = 0; + let at = -1; + for (let i = 0; i < occurrence; i += 1) { + at = text.indexOf(needle, from); + from = at + 1; + } + const before = text.slice(0, at); + const line = before.split('\n').length; + const column = at - (before.lastIndexOf('\n') + 1) + 1; + return { line, column }; +} + +interface FormatCheck { + name: string; + /** The clause of the specification this check comes from. */ + spec: string; + /** The plausible wrong implementation this check fails. */ + rulesOut: string; + run: () => number; +} + +const formatChecks: FormatCheck[] = [ + // ── HTML: the tree ────────────────────────────────────────────────────── + { + name: 'a-fragment-is-the-elements-written-and-nothing-implied', + spec: 'the grammar builds the elements written; the html, head and body a browser implies are not in the source', + rulesOut: 'a reader that invents wrapper rows, or that calls a snippet a whole page', + run: () => { + const x = parseHtml('

only a paragraph

'); + let bad = 0; + const tags = x.elements.map((e) => e.tagName).join(' '); + if (tags !== 'p') bad += fail(`elements=${tags}, expected p alone`); + if (x.document.documentKind !== HtmlDocumentKind.FRAGMENT) bad += fail(`documentKind=${x.document.documentKind}`); + const p = x.elements[0]!; + if (p.parentElementLinkHash !== '' || p.path !== '/p[1]' || p.depth !== 0) bad += fail(`a top-level element reads as ${p.path} at depth ${p.depth} with parent ${JSON.stringify(p.parentElementLinkHash)}`); + const whole = parseHtml('

x

'); + if (whole.document.documentKind !== HtmlDocumentKind.DOCUMENT || whole.document.doctype !== 'html') { + bad += fail(`a full document reads as ${whole.document.documentKind} with doctype ${JSON.stringify(whole.document.doctype)}`); + } + if (whole.elements.map((e) => e.path).join(' ') !== '/html[1] /html[1]/body[1] /html[1]/body[1]/p[1]') bad += fail(`paths=${whole.elements.map((e) => e.path).join(' ')}`); + const noDoctype = parseHtml(''); + if (noDoctype.document.documentKind !== HtmlDocumentKind.DOCUMENT) bad += fail('a page with and no doctype reads as a fragment'); + return bad; + }, + }, + { + name: 'a-paragraph-is-closed-by-the-next-one', + spec: 'a p element\'s end tag may be omitted if it is immediately followed by another p', + rulesOut: 'a reader that nests the second p inside the first', + run: () => { + const text = '
\n

one\n

two\n

'; + const x = parseHtml(text); + const ps = x.elements.filter((e) => e.tagName === 'p'); + let bad = 0; + if (ps.length !== 2) return fail(`${ps.length} p elements, expected 2`); + if (ps[0]!.path !== '/div[1]/p[1]' || ps[1]!.path !== '/div[1]/p[2]') { + bad += fail(`paths ${ps.map((p) => p.path).join(' ')}: the second p is a sibling, not a child`); + } + if (ps[0]!.endLine !== 3) bad += fail(`first p ends on line ${ps[0]!.endLine}, expected 3 where the second opens`); + if (ps[0]!.textContent !== 'one' || ps[1]!.textContent !== 'two') bad += fail('text content crossed the implicit close'); + if (ps[0]!.position !== 0 || ps[1]!.position !== 1) bad += fail('positions are not the sibling ordinals'); + return bad; + }, + }, + { + name: 'template-content-is-walked', + spec: 'a template element\'s contents are a DocumentFragment, not children of the element', + rulesOut: 'a walker that reads childNodes and finds the template empty', + run: () => { + const x = parseHtml(''); + const span = x.elements.find((e) => e.tagName === 'span'); + if (span === undefined) return fail('no span inside the template'); + let bad = 0; + if (span.path !== '/template[1]/span[1]') bad += fail(`path=${span.path}`); + if (!x.classReferences.some((c) => c.className === 'tpl')) bad += fail('the class inside the template was not read'); + return bad; + }, + }, + { + name: 'a-void-element-has-no-end-tag-and-no-children', + spec: 'void elements have no end tag and never have contents', + rulesOut: 'a reader that swallows the following text into the img', + run: () => { + const x = parseHtml('

after

'); + const img = x.elements.find((e) => e.tagName === 'img')!; + const p = x.elements.find((e) => e.tagName === 'p')!; + let bad = 0; + if (!img.isVoid) bad += fail('img is not void'); + const expectedEnd = '

'.length + 1; + if (img.endColumn !== expectedEnd) bad += fail(`img ends at column ${img.endColumn}, expected ${expectedEnd} (just past its start tag)`); + if (p.textContent !== 'after') bad += fail(`p text=${JSON.stringify(p.textContent)}`); + return bad; + }, + }, + { + name: 'foreign-content-keeps-its-namespace-and-prefixed-attributes', + spec: 'svg and MathML elements are in their own namespaces; xlink:href is a prefixed attribute', + rulesOut: 'a reader that lowercases viewBox or loses the xlink prefix', + run: () => { + const x = parseHtml(''); + const svg = x.elements.find((e) => e.tagName === 'svg')!; + let bad = 0; + if (svg.namespace !== HtmlNamespace.SVG) bad += fail(`svg namespace=${svg.namespace}`); + if (!x.attributes.some((a) => a.name === 'viewBox')) bad += fail('viewBox lost its case'); + const xlink = x.attributes.find((a) => a.prefix === 'xlink'); + if (xlink === undefined || xlink.name !== 'href' || xlink.attributeKind !== HtmlAttributeKind.URL) { + bad += fail('xlink:href is not a prefixed URL attribute'); + } + const ref = x.references.find((r) => r.attributeName === 'xlink:href'); + if (ref === undefined || ref.urlKind !== WebUrlKind.FRAGMENT || ref.fragment !== 'a') bad += fail('xlink:href="#a" is not a FRAGMENT reference to a'); + return bad; + }, + }, + // ── HTML: attributes ──────────────────────────────────────────────────── + { + name: 'a-value-less-attribute-is-distinguished-from-an-empty-one', + spec: 'an attribute without a value has the empty string as its value', + rulesOut: 'a reader that records both as value="" and cannot tell disabled from disabled=""', + run: () => { + const x = parseHtml(''); + const by = Object.fromEntries(x.attributes.map((a) => [a.name, a])); + let bad = 0; + if (by['disabled']?.hasValue !== false) bad += fail('disabled reports a value'); + if (by['required']?.hasValue !== true) bad += fail('required="" reports no value'); + if (by['value']?.hasValue !== true) bad += fail("value='' reports no value"); + if (by['data-x']?.value !== 'y' || by['data-x']?.attributeKind !== HtmlAttributeKind.DATA) bad += fail('data-x = "y" with spaces around = was misread'); + return bad; + }, + }, + { + name: 'a-duplicate-attribute-keeps-the-first-and-records-a-gap', + spec: 'the first attribute with a name wins; a repeat is a parse error', + rulesOut: 'a reader that emits two rows for one attribute name and collides their keys (the grammar reports no duplicate)', + run: () => { + const x = parseHtml('

'); + const ids = x.attributes.filter((a) => a.name === 'id'); + let bad = 0; + if (ids.length !== 1 || ids[0]!.value !== 'a') bad += fail(`id rows=${ids.map((a) => a.value).join(',')}, expected the first, a`); + if (!x.parseGaps.some((g) => g.gapKind === HtmlParseGapKind.PARSE_ERROR && g.detail === 'duplicate-attribute')) { + bad += fail('no duplicate-attribute gap'); + } + if (x.elements.find((e) => e.tagName === 'div')!.id !== 'a') bad += fail('element id is not the first'); + return bad; + }, + }, + { + name: 'class-tokens-split-on-ascii-whitespace-and-keep-repeats', + spec: 'the class attribute is a set of space-separated tokens; the tokenizer splits on space, tab, LF, FF, CR', + rulesOut: 'a reader that splits on a single space or deduplicates', + run: () => { + const x = parseHtml('
'); + const got = x.classReferences.map((c) => `${c.position}:${c.className}`).join(' '); + let bad = 0; + if (got !== '0:a 1:b 2:c 3:a') bad += fail(`tokens=${got}`); + if (new Set(x.classReferences.map((c) => c.getHash())).size !== 4) bad += fail('two occurrences of one class share a key'); + const div = x.elements.find((e) => e.tagName === 'div')!; + if (div.toCsv().split('\t')[5] !== 'a,b,c,a') bad += fail(`classNames column=${div.toCsv().split('\t')[5]}`); + return bad; + }, + }, + { + name: 'attribute-kinds-follow-the-name', + spec: 'on* attributes are event handler content attributes; data-* and aria-* are reserved prefixes', + rulesOut: 'a classifier that calls a Vue @click or an Alpine x-on an event handler', + run: () => { + const x = parseHtml(''); + const by = Object.fromEntries(x.attributes.map((a) => [a.name, a.attributeKind])); + const want: Record = { + onclick: HtmlAttributeKind.EVENT_HANDLER, '@click': HtmlAttributeKind.TEMPLATE_DIRECTIVE, 'v-if': HtmlAttributeKind.TEMPLATE_DIRECTIVE, + 'x-data': HtmlAttributeKind.TEMPLATE_DIRECTIVE, 'hx-get': HtmlAttributeKind.TEMPLATE_DIRECTIVE, 'th:text': HtmlAttributeKind.TEMPLATE_DIRECTIVE, + 'data-q': HtmlAttributeKind.DATA, 'aria-hidden': HtmlAttributeKind.ARIA, role: HtmlAttributeKind.ARIA, for: HtmlAttributeKind.FOR, + form: HtmlAttributeKind.ID_REFERENCE, rel: HtmlAttributeKind.REL, type: HtmlAttributeKind.TYPE, name: HtmlAttributeKind.NAME, + style: HtmlAttributeKind.STYLE, 'ng-if': HtmlAttributeKind.TEMPLATE_DIRECTIVE, '[prop]': HtmlAttributeKind.TEMPLATE_DIRECTIVE, + '(ev)': HtmlAttributeKind.TEMPLATE_DIRECTIVE, + }; + let bad = 0; + for (const [name, kind] of Object.entries(want)) { + if (by[name] !== kind) bad += fail(`${name}: ${by[name]}, expected ${kind}`); + } + // `@click="b"` is a template event: Vue calls the handler it names, and the row says so by its source. + const b = x.handlerCalls.find((h) => h.calleeName === 'b'); + if (b === undefined || b.handlerSource !== HtmlHandlerSource.TEMPLATE_EVENT || b.argumentCount !== 0) bad += fail('a template event directive is not a TEMPLATE_EVENT handler call'); + if (x.handlerCalls.some((h) => h.handlerSource === HtmlHandlerSource.EVENT_ATTRIBUTE && h.calleeName !== 'a')) bad += fail('a template directive was read as an on* handler'); + const dialects = [...x.document.templateDialects].sort().join(','); + if (!dialects.includes('ALPINE') || !dialects.includes('HTMX') || !dialects.includes('THYMELEAF') || !dialects.includes('VUE') || !dialects.includes('ANGULAR')) { + bad += fail(`dialects=${dialects}`); + } + return bad; + }, + }, + // ── HTML: URLs ────────────────────────────────────────────────────────── + { + name: 'url-kinds-are-decided-from-the-text', + spec: 'URL parsing: scheme, host-relative, path-absolute, path-relative and fragment-only forms', + rulesOut: 'a classifier that treats mailto: as a relative path or #x as a file', + run: () => { + const cases: Array<[string, WebUrlKind, string?]> = [ + ['a/b.css', WebUrlKind.RELATIVE], ['./a.js', WebUrlKind.RELATIVE], ['../x', WebUrlKind.RELATIVE], + ['/static/a.js', WebUrlKind.ROOT_RELATIVE], ['https://h/x.js', WebUrlKind.ABSOLUTE], ['HTTP://h/x', WebUrlKind.ABSOLUTE], + ['//cdn/x.js', WebUrlKind.PROTOCOL_RELATIVE], ['#team', WebUrlKind.FRAGMENT, 'team'], ['data:image/png;base64,AAA', WebUrlKind.DATA_URI], + ['javascript:go()', WebUrlKind.JAVASCRIPT_URI], ['mailto:a@b', WebUrlKind.OTHER_SCHEME], ['tel:123', WebUrlKind.OTHER_SCHEME], + ['{{ url_for("x") }}', WebUrlKind.TEMPLATE_EXPRESSION], ['/static/{{ name }}.js', WebUrlKind.TEMPLATE_EXPRESSION], + ['<%= path %>', WebUrlKind.TEMPLATE_EXPRESSION], ['${ctx}/x', WebUrlKind.TEMPLATE_EXPRESSION], ['', WebUrlKind.EMPTY], + [' ', WebUrlKind.EMPTY], + ]; + let bad = 0; + for (const [url, kind, fragment] of cases) { + const x = parseHtml(`x`); + const r = x.references[0]; + if (r === undefined) { bad += fail(`${JSON.stringify(url)}: no reference`); continue; } + if (r.urlKind !== kind) bad += fail(`${JSON.stringify(url)}: ${r.urlKind}, expected ${kind}`); + if (fragment !== undefined && r.fragment !== fragment) bad += fail(`${JSON.stringify(url)}: fragment=${r.fragment}`); + } + const split = parseHtml('x').references[0]!; + if (split.path !== 'page.html' || split.query !== 'x=1&y=2' || split.fragment !== 'top') { + bad += fail(`split: path=${split.path} query=${split.query} fragment=${split.fragment}`); + } + return bad; + }, + }, + { + name: 'a-relative-url-resolves-against-the-file-and-never-outside-the-root', + spec: 'a relative reference resolves against the base URL, which defaults to the document\'s', + rulesOut: 'a resolver that follows ../.. out of the project, or resolves to a file that is not there', + run: () => { + file('site/css/present.css', 'a{}'); + file('outside.css', 'b{}'); + const x = parseHtml('', 'site/page.html'); + const by = Object.fromEntries(x.references.map((r) => [r.urlAsWritten, r])); + let bad = 0; + if (by['css/present.css']?.resolvedFilePath !== path.join(TMP, 'site/css/present.css') || !by['css/present.css']?.isResolved) { + bad += fail('a present file was not resolved'); + } + if (by['css/absent.css']?.isResolved !== false) bad += fail('an absent file reads as resolved'); + if (by['../outside.css']?.isResolved !== false) bad += fail('a file outside the project root was resolved'); + if (x.document.toCsv().split('\t')[14] !== '3') bad += fail(`stylesheetReferenceCount=${x.document.toCsv().split('\t')[14]}, expected 3`); + return bad; + }, + }, + { + name: 'a-root-relative-url-resolves-only-when-one-served-root-holds-it', + spec: 'a path-absolute URL resolves against the origin, which the repository does not record', + rulesOut: 'a resolver that guesses between two candidate roots, or never resolves /static/x', + run: () => { + file('app/public/static/one.js', ''); + file('app/pages/deep/page.html', ''); + file('app/public/static/two.js', ''); + file('app/pages/static/two.js', ''); + const content = ''; + const p = file('app/pages/deep/page.html', content); + const x = new HtmlParser().parse(content, p, path.join(TMP, 'app'), 'V'); + const by = Object.fromEntries(x.references.map((r) => [r.urlAsWritten, r])); + let bad = 0; + if (by['/static/one.js']?.resolvedFilePath !== path.join(TMP, 'app/public/static/one.js')) { + bad += fail(`one candidate: resolved to ${JSON.stringify(by['/static/one.js']?.resolvedFilePath)}`); + } + if (by['/static/two.js']?.isResolved !== false) bad += fail('two candidates were not refused'); + if (by['/static/none.js']?.isResolved !== false) bad += fail('no candidate reads as resolved'); + const script = x.scripts.find((s) => s.src === '/static/one.js')!; + if (script.resolvedFilePath !== by['/static/one.js']!.resolvedFilePath || script.referenceLinkHash !== by['/static/one.js']!.getHash()) { + bad += fail('the script row does not carry its reference\'s resolution'); + } + return bad; + }, + }, + { + name: 'reference-kinds-follow-the-element', + spec: 'link rel=stylesheet is a stylesheet, source in picture is an image, elsewhere media', + rulesOut: 'a classifier that keys on the attribute name alone', + run: () => { + const x = parseHtml( + '' + + '' + + '
' + + '' + + '' + ); + const got = x.references.map((r) => `${r.referenceKind}:${r.urlAsWritten}`).sort().join(' '); + const want = [ + 'STYLESHEET:a.css', 'LINK_RESOURCE:i.png', 'STYLESHEET:b.css', 'IMAGE:p.webp', 'IMAGE:p.png', 'MEDIA:v.mp4', 'MEDIA:v.png', + 'MEDIA:v.webm', 'IMAGE:btn.png', 'OTHER:x', 'FORM_ACTION:/f', 'FORM_ACTION:/g', 'FRAME:f.html', 'FRAME:o.swf', 'BASE:/', + 'ANCHOR:l', 'ANCHOR:m', 'META_REFRESH:next.html', + ].sort().join(' '); + return got === want ? 0 : fail(`got ${got}\n want ${want}`); + }, + }, + { + name: 'srcset-candidates-are-separate-references', + spec: 'a srcset is a comma-separated list of candidates, each a URL and an optional descriptor', + rulesOut: 'a reader that records the whole attribute as one URL', + run: () => { + const x = parseHtml(''); + const got = x.references.map((r) => `${r.position}:${r.urlAsWritten}`).join(' '); + return got === '0:a.png 1:b.png 2:c.png 0:d.png' ? 0 : fail(`got ${got}`); + }, + }, + // ── HTML: handlers and scripts ────────────────────────────────────────── + { + name: 'every-call-in-a-handler-is-a-row-at-its-column', + spec: 'an event handler content attribute\'s value is a FunctionBody', + rulesOut: 'a reader that keeps the first call only, or cites the attribute instead of the call', + run: () => { + const text = '

\n x'; + const x = parseHtml(text); + const got = x.handlerCalls.map((h) => `${h.handlerSource === 'JAVASCRIPT_URL' ? 'url' : h.eventName}:${h.calleeText}/${h.calleeName}/${h.receiverText}/${h.argumentCount}${h.isNew ? '/new' : ''}`).join(' '); + const want = 'url:toggle/toggle//0 click:track/track//2 click:confirm/confirm//1 click:app.ui.remove/remove/app.ui/1 click:Audio/Audio//1/new'; + let bad = 0; + if (got !== want) bad += fail(`got ${got}\n want ${want}`); + const remove = x.handlerCalls.find((h) => h.calleeName === 'remove')!; + const at = col(text, 'app.ui.remove'); + if (remove.startLine !== at.line || remove.startColumn !== at.column) { + bad += fail(`remove cited at ${remove.startLine}:${remove.startColumn}, written at ${at.line}:${at.column}`); + } + const toggle = x.handlerCalls.find((h) => h.calleeName === 'toggle')!; + const tat = col(text, 'toggle()'); + if (toggle.startLine !== tat.line || toggle.startColumn !== tat.column) bad += fail(`toggle cited at ${toggle.startLine}:${toggle.startColumn}, written at ${tat.line}:${tat.column}`); + if (x.parseGaps.length !== 0) bad += fail(`gaps on well-formed handlers: ${x.parseGaps.map((g) => g.detail).join('; ')}`); + return bad; + }, + }, + { + name: 'a-handler-that-is-not-javascript-is-a-gap-not-a-call', + spec: 'a template placeholder in a handler is not a FunctionBody until rendered', + rulesOut: 'a reader that silently drops the attribute, or invents a call named handler', + run: () => { + const x = parseHtml(''); + let bad = 0; + const gaps = x.parseGaps.filter((g) => g.gapKind === HtmlParseGapKind.HANDLER_SYNTAX); + if (gaps.length !== 2) bad += fail(`${gaps.length} handler gaps, expected 2: ${x.parseGaps.map((g) => g.detail).join('; ')}`); + if (x.handlerCalls.some((h) => h.calleeName === 'handler')) bad += fail('a placeholder became a call'); + if (!x.handlerCalls.some((h) => h.calleeName === 'save')) bad += fail('the call the recovered tree still holds was dropped'); + return bad; + }, + }, + { + name: 'script-type-decides-what-the-browser-does-with-the-body', + spec: 'the type attribute: absent or a JavaScript MIME type runs; module; importmap; anything else is a data block', + rulesOut: 'a reader that runs every script or none', + run: () => { + const types: Array<[string, HtmlScriptType]> = [ + ['', HtmlScriptType.CLASSIC], ['text/javascript', HtmlScriptType.CLASSIC], ['application/javascript', HtmlScriptType.CLASSIC], + ['module', HtmlScriptType.MODULE], ['importmap', HtmlScriptType.IMPORTMAP], ['speculationrules', HtmlScriptType.SPECULATION_RULES], + ['application/json', HtmlScriptType.JSON], ['application/ld+json', HtmlScriptType.JSON], ['text/x-template', HtmlScriptType.TEMPLATE], + ['text/x-handlebars-template', HtmlScriptType.TEMPLATE], ['text/babel', HtmlScriptType.TRANSPILED], ['text/x-mathjax-config', HtmlScriptType.DATA_BLOCK], + ]; + let bad = 0; + for (const [type, want] of types) { + const x = parseHtml(`1`); + if (x.scripts[0]?.scriptType !== want) bad += fail(`type=${JSON.stringify(type)}: ${x.scripts[0]?.scriptType}, expected ${want}`); + } + return bad; + }, + }, + { + name: 'an-inline-script-body-is-located-exactly', + spec: 'script content is raw text between the start tag and the end tag', + rulesOut: 'a reader that trims the body or counts from the start tag', + run: () => { + const text = '\n\n \n'; + const x = parseHtml(text); + const [inline, external] = x.scripts; + let bad = 0; + if (inline === undefined || external === undefined) return fail(`${x.scripts.length} scripts`); + if (inline.scriptKind !== HtmlScriptKind.INLINE || !inline.isDefer) bad += fail('inline/defer misread'); + const bodyStart = text.indexOf(''); + const s = col(text, text.slice(bodyStart, bodyEnd)); + if (inline.bodyStartLine !== s.line || inline.bodyStartColumn !== s.column) bad += fail(`body starts ${inline.bodyStartLine}:${inline.bodyStartColumn}, expected ${s.line}:${s.column}`); + if (inline.bodyLength !== bodyEnd - bodyStart) bad += fail(`bodyLength=${inline.bodyLength}, expected ${bodyEnd - bodyStart}`); + if (inline.bodyEndLine !== 5 || inline.bodyEndColumn !== 3) bad += fail(`body ends ${inline.bodyEndLine}:${inline.bodyEndColumn}, expected 5:3`); + if (external.scriptKind !== HtmlScriptKind.EXTERNAL || external.bodyLength !== 0) bad += fail('an external script\'s body was read'); + if (x.document.toCsv().split('\t')[12] !== '2' || x.document.toCsv().split('\t')[13] !== '1') bad += fail('script counts are wrong'); + return bad; + }, + }, + // ── HTML: inline CSS ──────────────────────────────────────────────────── + { + name: 'a-style-element-is-a-stylesheet-at-the-page-s-lines', + spec: 'a style element\'s content is a CSS stylesheet', + rulesOut: 'a reader that cites line 1 of the CSS for a rule on line 4 of the page', + run: () => { + const text = '\n \n'; + const x = parseHtml(text); + let bad = 0; + if (x.stylesheets.length !== 1) return fail(`${x.stylesheets.length} stylesheets`); + const sheet = x.stylesheets[0]!; + const style = x.elements.find((e) => e.tagName === 'style')!; + if (sheet.sourceKind !== CssStylesheetSource.HTML_STYLE_ELEMENT || sheet.ownerHtmlElementLinkHash !== style.getHash()) bad += fail('the sheet does not chain to its element'); + if (sheet.startLine !== 2 || sheet.startColumn !== 10 || sheet.endLine !== 5) bad += fail(`sheet spans ${sheet.startLine}:${sheet.startColumn}-${sheet.endLine}`); + const b = x.css.rules.find((r) => r.preludeText === '.b')!; + if (b.startLine !== 4 || b.startColumn !== 5) bad += fail(`.b at ${b.startLine}:${b.startColumn}, expected 4:5`); + const gap = x.css.declarations.find((d) => d.property === 'gap')!; + if (gap.startLine !== 4 || gap.startColumn !== 10 || gap.stylesheetLinkHash !== sheet.getHash()) bad += fail(`gap at ${gap.startLine}:${gap.startColumn}`); + if (!x.css.valueReferences.some((v) => v.name === '--g' && v.startLine === 4)) bad += fail('var(--g) not read from the inline sheet'); + if (sheet.getRuleCount() !== 2 || sheet.getDeclarationCount() !== 2) bad += fail('counts not back-patched'); + const other = parseHtml(''); + if (other.stylesheets.length !== 0) bad += fail('a non-CSS style type was parsed as CSS'); + return bad; + }, + }, + { + name: 'a-style-attribute-is-a-declaration-list-owned-by-the-attribute', + spec: 'the style attribute\'s value is a ', + rulesOut: 'a reader that needs a rule to own a declaration, or drops the good half of a bad list', + run: () => { + const text = '

x
'; + const x = parseHtml(text); + const div = x.attributes.find((a) => a.name === 'style' && a.value.startsWith('color: red'))!; + const mine = x.css.declarations.filter((d) => d.htmlAttributeLinkHash === div.getHash()); + let bad = 0; + if (mine.map((d) => `${d.property}=${d.valueText}`).join(' ') !== 'color=red margin=0') bad += fail(`declarations=${mine.map((d) => d.property).join(',')}`); + if (mine.some((d) => d.ruleLinkHash !== '' || d.stylesheetLinkHash !== '')) bad += fail('a style attribute declaration claims a rule or a sheet'); + const at = col(text, 'margin'); + if (mine[1]!.startLine !== at.line || mine[1]!.startColumn !== at.column) bad += fail(`margin at ${mine[1]!.startLine}:${mine[1]!.startColumn}, written at ${at.line}:${at.column}`); + if (!x.css.declarations.some((d) => d.property === 'top')) bad += fail('the readable declaration after a bad one was dropped'); + if (!x.parseGaps.some((g) => g.gapKind === HtmlParseGapKind.STYLE_ATTRIBUTE_SYNTAX)) bad += fail('no STYLE_ATTRIBUTE_SYNTAX gap'); + return bad; + }, + }, + { + name: 'template-dialects-are-read-from-their-markers', + spec: 'the file is a template of the family whose delimiters it uses', + rulesOut: 'a detector keyed on the file extension, which is .html for all of them', + run: () => { + const cases: Array<[string, HtmlTemplateDialect[]]> = [ + ['

{{ a }}

', [HtmlTemplateDialect.MUSTACHE]], + ['{% if a %}

{{ a }}

{% endif %}', [HtmlTemplateDialect.JINJA, HtmlTemplateDialect.MUSTACHE]], + ['{{#each items}}
  • {{this}}
  • {{/each}}', [HtmlTemplateDialect.HANDLEBARS, HtmlTemplateDialect.MUSTACHE]], + ['<% if (u) { %>

    <%= u %>

    <% } %>', [HtmlTemplateDialect.ERB]], + ['', [HtmlTemplateDialect.PHP]], + ['@model Foo\n

    @Model.Name

    ', [HtmlTemplateDialect.RAZOR]], + ['

    ${name}

    ', [HtmlTemplateDialect.DOLLAR_BRACE]], + ['

    x

    ', [HtmlTemplateDialect.DOLLAR_BRACE, HtmlTemplateDialect.THYMELEAF]], + ['

    plain

    ', []], + ]; + let bad = 0; + for (const [text, want] of cases) { + const got = [...parseHtml(text).document.templateDialects].sort().join(','); + if (got !== [...want].sort().join(',')) bad += fail(`${JSON.stringify(text)}: ${got || '(none)'}, expected ${want.join(',') || '(none)'}`); + } + return bad; + }, + }, + { + name: 'gaps-are-capped-with-a-count-of-the-rest', + spec: 'a parse error per attribute on a file with thousands of them is a count, not a table', + rulesOut: 'an extractor that emits ten thousand gap rows, or drops them with no trace', + run: () => { + const attrs = Array.from({ length: WEB_PARSE_GAP_LIMIT + 50 }, (_, i) => `a${i}="1" a${i}="2"`).join(' '); + const x = parseHtml(`
    `); + const limit = x.parseGaps.find((g) => g.gapKind === HtmlParseGapKind.GAP_LIMIT_REACHED); + let bad = 0; + if (x.parseGaps.length !== WEB_PARSE_GAP_LIMIT + 1) bad += fail(`${x.parseGaps.length} gaps, expected ${WEB_PARSE_GAP_LIMIT} + 1`); + if (limit === undefined || !limit.detail.startsWith('50 ')) bad += fail(`limit row=${limit?.detail}`); + if (x.document.getParseGapCount() !== x.parseGaps.length) bad += fail('document gap count disagrees'); + return bad; + }, + }, + // ── CSS: selectors ────────────────────────────────────────────────────── + { + name: 'specificity-follows-selectors-level-4', + spec: 'count ids (a); classes, attributes and pseudo-classes (b); types and pseudo-elements (c); :where() adds nothing; :is()/:not()/:has() add their most specific argument', + rulesOut: 'a counter that counts :where(), ignores :not()\'s argument, or counts *', + run: () => { + const cases: Array<[string, string]> = [ + ['#a .b c::before', '1,1,2'], ['*', '0,0,0'], ['ul li', '0,0,2'], [':root', '0,1,0'], + [':is(#x, .y) p', '1,0,1'], [':where(.x) p', '0,0,1'], ['div:not(.a .b)', '0,2,1'], [':has(> img)', '0,0,1'], + ['li:nth-child(2n of .x)', '0,2,1'], ['a:hover', '0,1,1'], ['a:before', '0,0,2'], ['input[type="text"]', '0,1,1'], + [':host(.a)', '0,2,0'], ['.a.b.c', '0,3,0'], ['svg|rect', '0,0,1'], ['&:hover', '0,1,0'], + ]; + let bad = 0; + for (const [selector, want] of cases) { + const got = specificity(selector); + if (got !== want) bad += fail(`${selector}: ${got}, expected ${want}`); + } + return bad; + }, + }, + { + name: 'selector-parts-keep-compounds-combinators-and-nesting', + spec: 'a complex selector is compounds separated by combinators; a functional pseudo-class takes selectors as arguments', + rulesOut: 'a flattener that loses which compound a class is in, or whether it was negated', + run: () => { + const x = parseCss('.nav > li.item:not(.hidden) + a[href^="http" i] ~ b::before, #x { color: red }'); + const got = x.selectorParts.map((p) => `${p.position}:${p.partKind}:${p.name}${p.value ? '=' + p.value : ''}${p.attributeMatcher}${p.attributeFlags}@${p.compoundIndex}${p.combinatorBefore === CssCombinator.NONE ? '' : '<' + p.combinatorBefore}${p.depth ? '^' + p.depth : ''}`).join(' '); + const want = '0:CLASS:nav@0 1:TYPE:li@1