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 9ebdd1fb..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 @@ -104,8 +133,8 @@ jobs: fail-fast: false matrix: target: - - { os: ubuntu-24.04, platform: linux-x64 } - - { os: ubuntu-24.04-arm, platform: linux-arm64 } + - { os: ubuntu-24.04, platform: linux-x64, manylinux: quay.io/pypa/manylinux_2_28_x86_64 } + - { os: ubuntu-24.04-arm, platform: linux-arm64, manylinux: quay.io/pypa/manylinux_2_28_aarch64 } - { os: windows-2025, platform: win32-x64 } runs-on: ${{ matrix.target.os }} steps: @@ -119,14 +148,31 @@ jobs: path: engines key: engines-${{ matrix.target.platform }}-${{ needs.generate.outputs.flags }}-${{ needs.generate.outputs.key }} restore-keys: engines-${{ matrix.target.platform }}-${{ needs.generate.outputs.flags }}- - - name: Compile every language (Linux) + # THE GLIBC FLOOR. A binary links against the glibc of the machine that built it, and runs only where that + # glibc or a newer one is installed. Built on ubuntu-24.04 (glibc 2.39) every engine needed GLIBC_2.38, so it + # would not start on Ubuntu 22.04, Debian 12, RHEL 9 or Amazon Linux 2023: the official python and node + # Docker images, most CI and most servers. The compile therefore runs inside manylinux_2_28 (glibc 2.28, + # the floor Python wheels use), and the step after it fails the build if any binary asks for more. + - name: Compile every language (Linux, manylinux_2_28) if: startsWith(matrix.target.platform, 'linux') + env: + MANYLINUX: ${{ matrix.target.manylinux }} run: | set -e + docker run --rm -v "$PWD:/w" -w /w -e LANGUAGES="$LANGUAGES" -u "$(id -u):$(id -g)" "$MANYLINUX" bash -ec ' + c++ --version | head -1; ldd --version | head -1 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 @@ -136,7 +182,19 @@ jobs: c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "$cpp" -o "engines/queries/axiomcode-query-$q" cp "gen/queries/$q.id" "engines/queries/$q.id" done + ' ls -la engines/*; ldd engines/java/axiomcode-engine-java || true + - name: Linux binaries need no glibc newer than 2.28 + if: startsWith(matrix.target.platform, 'linux') + run: | + set -e + bad=0 + for f in engines/*/axiomcode-*; do + need="$(objdump -T "$f" | grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -V | tail -1)" + echo "$f needs glibc $need" + if [ "$(printf '%s\n2.28\n' "$need" | sort -V | tail -1)" != 2.28 ]; then echo "::error::$f needs glibc $need (> 2.28)"; bad=1; fi + done + exit $bad - uses: ilammy/msvc-dev-cmd@v1 if: startsWith(matrix.target.platform, 'win32') with: { arch: x64 } @@ -148,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 0c000310..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' @@ -253,7 +253,7 @@ jobs: needs: [changes] if: needs.changes.outputs.code == 'true' runs-on: ubuntu-24.04 - timeout-minutes: 45 + timeout-minutes: 90 env: # the engine is compiled for any x86-64 runner, so a cached one can be restored # on whichever runner this job lands (see the engine cache step below) @@ -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: @@ -448,6 +458,36 @@ jobs: # AXIOM_SUITE_JOBS=1 here would run them one at a time, as they used to. run: bash .github/scripts/run-suite.sh ${{ matrix.lang }} ${{ matrix.oracle }} + # THE QUERY LAYER'S CASES (tests/run.py): what impact, path, context, changed and test-impact answer on small + # projects written for one behaviour each. Every case must pass, and a case marked pending that now passes fails + # the run until the mark is removed, so a fix for one shape cannot quietly break another's answer. The suite + # calls the verbs directly, so it checks their own answers, not the front-door rendering (tests/front_door.py). + - name: ${{ matrix.lang }} query cases + env: + AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js + AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache + run: python3 tests/run.py --lang ${{ matrix.lang }} --jobs 4 + + # the small surface as users and agents get it: find / impact / path / tests through the installed command and + # the MCP server, answered as places with their code, and the direct calls and flags that keep the old answers + - name: the front door answers as places with their code + if: matrix.lang == 'python' + env: + AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js + AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache + run: python3 tests/front_door.py + + # The hooks answer impact from SQL (graph_sql.impact_shaped) and fall back to the rules (dl/impact.dl) only when + # it declines, so the two must list the same rows for the same edit: tests/fastpath.py indexes a small case, asks + # both on each target shape, and runs hooks/changes.py on one edit through each path. Same parser and engine + # cache as the suite above. Not typescript: its case needs the TypeScript engine; javascript has no case. + - name: the hooks' fast path agrees with the rules (${{ matrix.lang }}) + if: matrix.lang == 'python' || matrix.lang == 'java' || matrix.lang == 'csharp' + env: + AXIOM_PARSER: ${{ github.workspace }}/parser/dist/index.js + AXIOM_SOUFFLE_CACHE: ${{ github.workspace }}/.souffle-cache + run: python3 tests/fastpath.py --lang ${{ matrix.lang }} + # Saved whether or not the suite passed. The binary does not depend on the verdict: # run-souffle.sh publishes it only whole and verified (a temp name, then a rename), # so a red run's engine is as good as a green one's, and the run that most needs the diff --git a/README.md b/README.md index 3bd2a018..4879d548 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

@@ -100,7 +100,7 @@ That matters because an agent follows edges several hops deep, and one missed li axiomcode graph of an open-source TypeScript web framework, 366 files and 8,657 call edges. Source files form the inner ring, test files the outer ring. A change to basicAuth reaches 7 test files through resolved calls (solid blue); the other 130 test files have no chain to it (dashed red). basicAuth calls a shared compare function (green) that 11 other files also reach (gold).

-*`axiomcode graph` on an open-source TypeScript web framework, asked which tests a change to `basicAuth` can +*The call graph of an open-source TypeScript web framework, drawn as a page and asked which tests a change to `basicAuth` can affect. Source files form the inner ring and test files the outer one. The solid blue paths are chains of resolved calls from `basicAuth` to the 7 test files that must run; the dashed red ones mark the other 130, which have no chain to it and can be skipped. Green is the shared `compare` that `basicAuth` calls, and gold the 11 other files @@ -123,9 +123,9 @@ established relationships that could lead to false positives. AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) parses a repository and builds its graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the -agent-facing frontend: a skill, seven MCP tools, and hooks. Install the engine first. +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: @@ -200,56 +200,63 @@ axiomcode path main Ledger.put axiomcode path main SqlStore.put ``` -``` -main → Ledger.put: 1 of 1 target(s) reached through resolved calls; nearest at 2 hop(s) - 2 call(s): - main src/main.ts:6 - → [known_edge · call @ src/main.ts:9] OrderService.place src/orders/orderService.ts:7 - → [known_edge · call @ src/orders/orderService.ts:8] Ledger.put src/ledger/ledger.ts:4 - verified: every printed hop is an edge in the graph and a second, independent traversal finds the same length - -main → SqlStore.put: 1 of 1 target(s) reached through resolved calls; nearest at 2 hop(s) - 2 call(s): - main src/main.ts:6 - → [known_edge · call @ src/main.ts:9] OrderService.place src/orders/orderService.ts:7 - → [multi_inferred · call @ src/orders/orderService.ts:9] SqlStore.put src/storage/sqlStore.ts:6 - verified: every printed hop is an edge in the graph and a second, independent traversal finds the same length - what the hops are: - [known_edge] resolved to one declaration - [multi_inferred] several declarations fit; each is a real candidate -``` - -The first chain is `known_edge` all the way: each call has exactly one target. The second ends in -`multi_inferred`, because `store.put` can run `SqlStore.put` or `MemoryStore.put`, depending on which store `main` -built; the graph keeps both as candidates instead of picking one. +Every answer is a numbered list of places, each with the code of the function it sits in and the line that matters +marked `→`: + +```` +1. src/main.ts:9 [resolved · hop 1/2 main → OrderService.place] + ```typescript + 6 export function main(useSql: boolean): void { + 7 const store = useSql ? new SqlStore("orders") : new MemoryStore(); + 8 const service = new OrderService(new Ledger(), store); + → 9 service.place("A-1", 42); + 10 } + ``` +2. src/orders/orderService.ts:8 [resolved · hop 2/2 OrderService.place → Ledger.put] + ```typescript + 7 place(id: string, amount: number): void { + → 8 this.ledger.put(`order ${id}: ${amount}`); + 9 this.store.put(id, amount); + 10 } + ``` +verified: ✓ (2 edge(s) looked up again) +```` + +The second chain ends the same way, at `src/orders/orderService.ts:9`, tagged `one of a set`: `store.put` can run +`SqlStore.put` or `MemoryStore.put`, depending on which store `main` built, and the graph keeps both as candidates +instead of picking one. From an agent, ask in plain words. The skill tells the agent to query the graph instead of grepping: -``` +```` > What breaks if I change SqlStore.put? - axiomcode_impact("SqlStore.put") - must change with it (1: bound by a contract the engine resolved): - Store.put src/storage/store.ts:2 — it implements this - reads or uses it (3 callable(s): 1 one of a set, 2 alongside): - [one of a set] OrderService.place src/orders/orderService.ts:9 — calls it - ... - reaches those through resolved calls: 4 more callable(s) in 3 file(s) - src/main.ts: main → OrderService.place - tests: 1 of 1 test method(s) reach the change - test files: test/orderService.test.ts (1) - verified: 2 printed edge(s) looked up again in the graph, all present -``` - -The change reaches the entry point and the test through a call that never names `SqlStore`. - -Each hop carries the line the call is on, how certain the edge is, and what kind of call it is. Every printed -edge is looked up again in the graph before you see it; the `verified:` line is that check reporting. + impact("SqlStore.put") + 1. src/storage/store.ts:2 [must change · it implements this] + ```typescript + → 2 put(key: string, value: number): void; + ``` + 2. src/orders/orderService.ts:9 [one of a set · OrderService.place] + ```typescript + 7 place(id: string, amount: number): void { + 8 this.ledger.put(`order ${id}: ${amount}`); + → 9 this.store.put(id, amount); + 10 } + ``` + 3. src/main.ts:6 [hop 2] + ... + 4. test/orderService.test.ts:3 [test · one of a set · hop 3] + ... + verified: ✓ (3 edge(s) looked up again) +```` + +The change reaches the entry point and the test through a call that never names `SqlStore`. Every printed edge is +looked up again in the graph before you see it; the `verified:` line is that check reporting. ### Support for agents -Every agent below gets the seven 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 | |---|---|---|---| @@ -301,40 +308,44 @@ about one change had to read 95 of 43,793 methods, and every true direct caller ## CLI commands -| command | what it does | +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 path ` | the chain of calls from A to B, hop by hop. `'*'` as one end gives the whole closure | -| `axiomcode impact ` | everything that has to be looked at again when a declaration changes, each labelled with how certain it is. `--tests` adds the tests that reach it | -| `axiomcode test-impact` | which tests have to run for the current edit, with the chain that reaches each | -| `axiomcode changed` | which declarations an edit changed, and how (signature, type, body, added, removed). `--impact` adds what that reaches | -| `axiomcode context ""` | where a task's words land in the code, when you have a problem statement and not yet a name | -| `axiomcode graph` | the whole graph as one self-contained HTML page, at `.axiomcode/graph/graph.html`, drawn from the existing graph (rebuilt first only when stale, with the flags it was indexed with) | -| `axiomcode index` | build or rebuild the graph explicitly; `--lang`, `--src` and `--library` narrow it | -| `axiomcode mcp` | serve the graph to an agent as MCP tools over stdio | - -A target is written the way it appears in the code: `Owner.method`, `method`, `Type`, `Owner.field`, or -`file.py:123`. It is resolved exactly; a miss lists the nearest names. `--range ..` compares two commits. The -query commands take `--json`. `axiomcode help ` prints one command's usage. +| `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 | +| `axiomcode tests` | the tests your uncommitted edits reach, and a last `run:` line with the command that runs them | +| `axiomcode link ` | record where a call the graph could not resolve lands (kept in `axiomcode-links.tsv`); impact, path and tests then walk it, labelled `[asserted]`. Alone, lists the links and whether each was applied | +| `axiomcode index` | build the graph explicitly (the first query builds it too); `--lang`, `--src` and `--library` narrow it | + +A name is written the way it appears in the code: `Owner.method`, `method`, `Type`, `Owner.field`, or +`file.py:123`. It is resolved exactly; a miss lists the nearest names. `axiomcode help ` prints one +command's usage. > [!NOTE] -> `changed` and `test-impact` compare the working tree with a **baseline**: the last commit (right after an explicit -> `axiomcode index`, the tree it indexed). The background refresh (below) resets it whenever HEAD moves (a commit, a -> merge, a pull, a checkout), so committed edits drop out and nothing accumulates; the two commands wait up to 30 s -> for that. While edits are uncommitted, they read the baseline's own graph, kept in `.axiomcode/base`, so a removed -> method still shows all its callers. +> `impact` with no name and `tests` compare the working tree with a **baseline**: the last commit (right after an +> explicit `axiomcode index`, the tree it indexed). The background refresh (below) resets it whenever HEAD moves (a +> commit, a merge, a pull, a checkout), so committed edits drop out and nothing accumulates. While edits are +> uncommitted, they read the baseline's own graph, kept in `.axiomcode/base`, so a removed method still shows all its +> callers. 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 ends with a `graph refresh: OFF` line naming the files it predates. When a name asked about finds nothing and an edit since the graph was built writes that name, the line says so, since the declaration may simply be too new -for the graph. The log is `.axiomcode/refresh.log`. +for the graph. A query that does start a rebuild says so on its answer's first line, with the reason. The +hooks and the MCP server's timer never rebuild a graph another axiomcode built (another engine, other rules or another +`IMPACT_VERSION` in its build stamp); a hook says so once per session. The log is `.axiomcode/refresh.log`. ## Graph output diff --git a/bin/axiomcode b/bin/axiomcode index 3e1d32c8..99f3f668 100755 --- a/bin/axiomcode +++ b/bin/axiomcode @@ -1,6 +1,10 @@ #!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── -# axiomcode — build a call graph from a source tree. +# axiomcode — ask a repository's call graph. `axiomcode --help` prints the dispatcher's help +# (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. # # Usage: # bin/axiomcode [--library [,…]] [--exclude-tests] [--version V] @@ -25,12 +29,18 @@ # # Engine options: # --dispatch-cap --lib-depth --jdk-depth --taint +# --closed-world narrow the dispatch fan to constructed types (TypeScript), recording +# each dropped edge as an assumption row # # Commands: # parser [--version V] [--exclude-tests] # Parse only; writes //. # engine --language L --client-ir / --out [options] # Solve previously parsed IR. +# prepare [--language L[,…]] [--background] +# Make each language's engine binary ready (packaged, cached, or compiled +# now), so the first index does not pay the compile. `npm run build` runs it +# in the background; AXIOMCODE_NO_PREPARE=1 or CI skips that. # test [java|typescript|python|javascript|csharp|parser|all] [suite options] # Run the test suites. # mcp Serve the graph to an agent as MCP tools over stdio. Any MCP client @@ -86,31 +96,30 @@ PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}" # (`--verbs`, derived from its own dispatch table) so the two entry points cannot drift: a new # capability is a new row in one case statement, not a new copy here. QUERY="$ROOT/plugins/axiomcode/skills/axiomcode/scripts/axiomcode" -# `tests` is the frontend's alias for test-impact; `test` is THIS command's engine suite. One letter -# apart, entirely different jobs, and putting both on one command is a trap — so the alias is not -# routed here, and the name is refused below with both readings spelled out. -# 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. +# `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 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 [ "$x" = tests ] || 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; } is_query_verb(){ load_verbs; local x; for x in ${QVERBS[@]+"${QVERBS[@]}"}; do [ "$x" = "$1" ] && return 0; done; return 1; } usage(){ - awk 'NR > 2 && /^# ─/ {exit} NR > 2' "$0" | sed 's/^# \{0,1\}//' - if [ -f "$QUERY" ]; then - echo - echo 'ASKING THE GRAPH — `axiomcode help ` for any one of them:' - bash "$QUERY" --help | sed -n 's/^ \(axiomcode [a-z].*\)$/ \1/p' - fi + # the query surface is the dispatcher's own help, so the two cannot drift; the build commands above are internal + if [ -f "$QUERY" ]; then bash "$QUERY" --help + else awk 'NR > 2 && /^# ─/ {exit} NR > 2' "$0" | sed 's/^# \{0,1\}//'; fi } +# the verbs the help advertises (` axiomcode ` lines), for the message a typo gets +public_verbs(){ [ -f "$QUERY" ] && bash "$QUERY" --help | sed -n 's/^ axiomcode \([a-z][a-z-]*\).*/\1/p'; return 0; } die(){ echo "axiomcode: $*" >&2; exit 2; } need_parser(){ [ -f "$PARSER" ] || { echo "axiomcode: parser not built at $PARSER — run: npm install && npm run build" >&2; exit 1; }; } @@ -127,25 +136,22 @@ case "$cmd" in if [ $# -gt 1 ] && is_query_verb "$2"; then exec bash "$QUERY" help "$2"; fi ;; "") ;; # THE MCP SERVER IS THE PLUGIN'S, NOT A SECOND ONE. The package already ships plugins/axiomcode/, - # so an agent that is not Claude Code gets the same seven tools from one line of MCP config and + # so an agent that is not Claude Code gets the same four tools from one line of MCP config and # no plugin install. launch.js picks the interpreter and falls back to a built-in protocol # implementation, so nothing beyond the python3 the query verbs already need has to be installed. mcp) shift export AXIOMCODE_PLUGIN_ROOT="$ROOT/plugins/axiomcode" exec node "$ROOT/plugins/axiomcode/mcp/launch.js" "$@" ;; - tests) echo "axiomcode: 'tests' is ambiguous here — 'axiomcode test-impact' for the tests an edit needs," >&2 - echo " 'axiomcode test' for this package's own engine suite." >&2; exit 2 ;; - *) if is_query_verb "$cmd"; then shift; exec bash "$QUERY" "$cmd" "$@"; fi ;; + # the installed command is a front door: a query asked here with no flags answers as places with their code + *) if is_query_verb "$cmd"; then shift; export AXIOMCODE_FRONT=1; exec bash "$QUERY" "$cmd" "$@"; fi ;; esac # A NAME THAT IS NOT A VERB AND NOT A DIRECTORY IS A TYPO, NOT A BUILD. `*) cmd=all` is convenient # for `axiomcode ./src out/` and wrong for everything else: a misspelled verb used to be parsed as a # source tree and fail with advice about building the parser. case "$cmd" in - parser|engine|all|test|-h|--help|help|"") [ $# -gt 0 ] && shift;; + parser|engine|prepare|all|test|-h|--help|help|"") [ $# -gt 0 ] && shift;; *) [ -d "$cmd" ] || { echo "axiomcode: '$cmd' is neither a verb nor a directory." >&2 - echo " build: parser engine all test (or: axiomcode )" >&2 - echo " serve: mcp" >&2 - echo " ask: $(query_verbs | tr '\n' ' ')" >&2 + echo " ask: $(public_verbs | tr '\n' ' ')" >&2 echo " \`axiomcode help\` for what each one does." >&2; exit 2; } cmd=all;; esac @@ -188,6 +194,37 @@ case "$cmd" in has --library || args+=(--library "") bash "$ROOT/graph/pipeline/run-souffle.sh" "${args[@]}" ;; + prepare) + # EVERY LANGUAGE'S ENGINE BINARY, BEFORE THE FIRST INDEX ASKS FOR IT. On a cache miss the engine is compiled to C++ + # and built by the C++ compiler: minutes, whatever the size of the repository, while the index it serves takes + # seconds. Left to the first index, that cost landed on whoever indexed first after an install or a rule change, + # and on a query that had to build a graph. `npm run build` runs this in the background (postbuild), so an install + # or a refresh starts the compiles; an index that starts meanwhile waits on the same lock instead of compiling twice. + langs=""; bg=false + while [ $# -gt 0 ]; do case "$1" in + --language) langs="$2"; shift 2;; --background) bg=true; shift;; + *) die "usage: axiomcode prepare [--language L[,…]] [--background]";; esac; done + if [ -z "$langs" ]; then + for d in "$ROOT"/graph/*/engine; do [ -d "$d" ] && { d="${d%/engine}"; langs="$langs,${d##*/}"; }; done + langs="${langs#,}" + fi + IFS=',' read -ra want <<< "$langs" + for l in ${want[@]+"${want[@]}"}; do [ -d "$ROOT/graph/$l/engine" ] || die "prepare: no engine for --language=$l"; done + if $bg; then + # a CI job compiles the engines it tests itself, with its own cache and -march; a background compile there only + # competes with the job for the machine + if [ -n "${AXIOMCODE_NO_PREPARE:-}" ] || [ -n "${CI:-}" ]; then echo "▶ engines not prepared (${AXIOMCODE_NO_PREPARE:+AXIOMCODE_NO_PREPARE}${CI:+CI} is set)"; exit 0; fi + log="${TMPDIR:-/tmp}/axiomcode-prepare-$(id -u 2>/dev/null || echo 0).log" + nohup bash "$0" prepare --language "$langs" > "$log" 2>&1 < /dev/null & + echo "▶ preparing the engines for ${langs//,/, } in the background (log: $log)" + exit 0 + fi + # one compile per language, side by side: each is one single-threaded compiler process of a few hundred MB + ncpu="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)"; jobs="${AXIOM_PREPARE_JOBS:-$(( ncpu / 2 ))}" + [ "$jobs" -ge 1 ] 2>/dev/null || jobs=1; [ "$jobs" -le ${#want[@]} ] || jobs=${#want[@]} + printf '%s\n' "${want[@]}" | xargs -P "$jobs" -I{} bash -c \ + 'set -o pipefail; bash "$1" --language "$2" --prepare 2>&1 | sed "s/^/[$2] /"' _ "$ROOT/graph/pipeline/run-souffle.sh" {} + ;; all) need_parser lang=""; src=""; out=""; version=""; libs=""; progress=""; popts=(); rest=(); pos=() @@ -195,7 +232,7 @@ case "$cmd" in --language) lang="$2"; shift 2;; --src) src="$2"; shift 2;; --out) out="$2"; shift 2;; --library) libs="$2"; shift 2;; --progress) progress="$2"; shift 2;; --version|--slug) version="$2"; shift 2;; --exclude-tests) popts+=(--exclude-tests); shift;; - --intermediate|--dispatch-cap|--lib-depth|--jdk-depth|--taint) rest+=("$1" "$2"); shift 2;; + --intermediate|--dispatch-cap|--lib-depth|--jdk-depth|--taint|--closed-world) rest+=("$1" "$2"); shift 2;; --debug) rest+=(--debug); shift;; --*) die "unknown option $1 (try --help)";; *) pos+=("$1"); shift;; esac; done @@ -258,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; } @@ -288,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 @@ -311,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/gemini-extension.json b/gemini-extension.json index 9b59561d..ebb25063 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.8", + "version": "0.1.9", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "contextFileName": "plugins/axiomcode/AGENTS.md", "mcpServers": { diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 625aac8d..0afb5407 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -215,6 +215,7 @@ What produced this bundle: one key/value row per fact about the run (language, e | `client_ir` | all | Path of the client IR directory the engine read. | | `library_roots` | all | Comma-separated library IR roots staged as the type oracle; empty for a client-only run. | | `dispatch_cap` | all | Fan-width cap on virtual dispatch in effect; `off` when uncapped. | +| `dispatch_closed_world` | all | `on` when the dispatch fan was narrowed to types the analysed code constructs (a closed-world premise; each dropped edge is an assumption row); `off` otherwise. Read by TypeScript. | | `jdk_depth` | all | Platform-library hop cap (engine-ii). | | `lib_depth` | all | External-library hop cap; `uncapped` when unset. | | `engine_ii` | all | `on` when the library-frontier forward chain (engine-ii) was included; `off` for a client-only solve. | @@ -441,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` @@ -466,6 +467,10 @@ 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. | +| `using_dispose` | csharp | SYNTHESISED. The `Dispose()` a `using` statement or declaration runs when its scope ends. FromExpr is the resource expression. | +| `foreach_enumerator` | csharp | SYNTHESISED. The `GetEnumerator()` a `foreach` runs on the iterated collection. FromExpr is the collection expression. | +| `collection_add` | csharp | SYNTHESISED. The `Add(...)` a collection initializer (`new Bag { 1, { 2, 3 } }`) runs for each element. FromExpr is the element. | | `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. | @@ -530,9 +535,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** @@ -556,6 +565,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `fan_capped` | javascript, java, csharp | More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count. | | `callback_registered` | javascript, typescript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. | | `event_dispatch` | javascript | `x.emit('name')` reaching a handler registered by `x.on('name', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one. | +| `event_dispatch` | typescript | A publish on a project class's keyed registry reaching each function filed under the same key: `bus.publish('x', p)` where `publish` (or a method it passes the key to) calls what `this.handlers.get(name)` holds, to the function a `bus.subscribe('x', f)` stored there, or an entry `{ ['x']: f }` of a table a subscribe-all method files by key. A literal, a const or a const-object member key; a key only known at run time matches every key, and a constant the dispatch also looks up (a wildcard) matches every publish. The site is the publish call (call-edge-generation/keyed_registry.dl). | | `event_dispatch` | java | A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e's static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity's type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl). | | `intrinsic_terminal` | typescript | The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name. | @@ -576,7 +586,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h - **python** — 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. - **java** — 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. - **python** — The reason a site is ambiguous_unknown is exported per site in ext_call_site_unresolved (site, caller, reason, detail). -- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. +- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. - **java** — A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local's initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy. - **typescript** — A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too. - **python** — A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript. @@ -645,10 +655,10 @@ Methods the runtime invokes without a client call site — process roots, test m | `main` | java, csharp | A static `main`. C#: a static `Main`, or the method top-level statements compile to. | | `test` | java, typescript, csharp | Java: a JUnit test or lifecycle method. TypeScript: a function body handed to a test registrar (`it`, `describe`), inline or named, which the runner invokes. C#: an xUnit, NUnit or MSTest test method, or a set-up or tear-down hook of one. | | `http` | java, python, typescript, csharp | A route handler a web framework invokes on a request. Java: a JAX-RS / Spring MVC handler. Python: a function registered with a decorator naming an HTTP verb and a URL path. TypeScript: a handler passed to a route registration (`app.get('/x', h)`), inline or named, or a method carrying a route decorator inside a container-owned class (`@Controller` + `@Get`). C#: a routed controller action, or a Razor Pages page model's `On[Handler][Async]` method. | -| `cli` | java | A CLI command method (picocli etc.). | +| `cli` | java, python | A CLI command method. Java: picocli and the like. Python: the handler of a management command, a `Command` class in a `management/commands/` module, which the command runner calls by the module's file name. | | `bean_ctor` | java, typescript | Constructor of a container-managed class. TypeScript: the class carries a framework decorator (`@Injectable`, `@Component`, `@Module`), so the container constructs it and nothing in the repository does. | | `factory` | java | A `@Bean` factory method. | -| `lifecycle` | java, typescript, csharp | Java: `@PostConstruct` / `@PreDestroy` and similar hooks. TypeScript: a hook the container calls by name on a decorated class (`ngOnInit`, `onModuleInit`), which has no call site anywhere. C#: a method the host calls on a hosted service (`ExecuteAsync`, `StartAsync`, `StopAsync`, and the `IHostedLifecycleService` hooks), including one that derives from the host's base through the project's own base class. | +| `lifecycle` | java, typescript, csharp, python | Python: a callable an application registers to run around requests, on an error or at start and stop: a middleware, an exception handler, a startup or shutdown handler, a lifespan. Java: `@PostConstruct` / `@PreDestroy` and similar hooks. TypeScript: a hook the container calls by name on a decorated class (`ngOnInit`, `onModuleInit`), which has no call site anywhere. C#: a method the host calls on a hosted service (`ExecuteAsync`, `StartAsync`, `StopAsync`, and the `IHostedLifecycleService` hooks), including one that derives from the host's base through the project's own base class. | | `queue` | java, csharp | A message-listener method. Java: also a Spring application event listener (`@EventListener`, `@TransactionalEventListener`, an `ApplicationListener` implementation). C#: a broker consumer or a bus message handler. | | `scheduled` | java | A `@Scheduled` method. | | `grpc_service` | java, python, csharp | A gRPC service implementation the server invokes on a request, with no call site reaching it: a generated `ImplBase` override (Java); a class deriving from a generated `*Servicer` base in a `_pb2_grpc` module, overriding a method that base declares (Python). | @@ -669,8 +679,8 @@ Methods the runtime invokes without a client call site — process roots, test m | `fixture` | python | A declared fixture that some collected test requests by parameter name. The runner calls it to build the argument. | | `url` | python | A view named as a value in a module-level route table. The framework calls it on a request. | | `signal_receiver` | python | A handler attached to a signal, by decorator or by connect(). It runs when the signal fires, whether or not this tree contains the send. | -| `di_provider` | python | A provider named in a dependency-injection marker in a parameter default. The framework calls it and passes the result in. | -| `orm_hook` | python, csharp | A hook the data layer calls; nothing in the client does. Python: a lifecycle or validation hook registered by decoration. C#: an Entity Framework Core override or implementation: `OnModelCreating`, `OnConfiguring`, `IEntityTypeConfiguration.Configure`, a migration's `Up`/`Down`, a save-changes interceptor, `IDesignTimeDbContextFactory.CreateDbContext`. | +| `di_provider` | python | A provider named in a dependency-injection marker: a parameter default, `Annotated` metadata (in place or through an alias), or a `dependencies=[...]` list on a route or router. A def, a class (its constructor) or a callable instance (its `__call__`). The framework calls it and passes the result in. | +| `orm_hook` | python, csharp | A hook the data layer calls; nothing in the client does. Python: a lifecycle, validation or serialization hook registered by decoration, or a `model_post_init` override on a model class. C#: an Entity Framework Core override or implementation: `OnModelCreating`, `OnConfiguring`, `IEntityTypeConfiguration.Configure`, a migration's `Up`/`Down`, a save-changes interceptor, `IDesignTimeDbContextFactory.CreateDbContext`. | | `framework_hook` | csharp | A method a framework calls on a class because the class derives from one of its base types or implements one of its interfaces, directly or through the project's own bases: an options setup class, a view component, an authorization handler, a model binder, a gRPC interceptor, a FluentValidation validator's constructor and overrides, a MediatR request or notification handler, `Dispose`/`DisposeAsync` on an `IDisposable`/`IAsyncDisposable`. | | `hub` | csharp | A public instance method of a SignalR hub. A connected client invokes it by name. | @@ -759,29 +769,30 @@ THE DATA GRAPH. One row per (site, resolved field), and the answer to "who reads | value | languages | meaning | |---|---|---| -| `read` | java, typescript | The value is used and not replaced. | -| `write` | java, typescript | The value is replaced without being read: a plain assignment `f = v`. | -| `readwrite` | java, typescript | The value is read and replaced at the one site: a compound assignment `f += v`, or `f++` / `--f`. One row, not two — a consumer asking "who writes f" and one asking "who reads f" must both match it. | +| `read` | java, typescript, csharp | The value is used and not replaced. | +| `write` | java, typescript, csharp | The value is replaced without being read: a plain assignment `f = v`. | +| `readwrite` | java, typescript, csharp | The value is read and replaced at the one site: a compound assignment `f += v`, or `f++` / `--f`. One row, not two — a consumer asking "who writes f" and one asking "who reads f" must both match it. | **`field_access.tier` values** | value | languages | meaning | |---|---|---| -| `known_edge` | java, typescript | Exactly one field resolved. Stronger than the call_edges tier of the same name: a field is not virtually dispatched, so this IS the storage location the access binds to. | -| `multi_inferred` | java, typescript | A sound SET: the receiver has more than one possible type, or two unrelated ancestors declare the name (which Java itself treats as ambiguous). Each member is one row. | -| `boundary_lib` | java, typescript | The field is declared in a staged library type. field_id is set and resolves in `fields` with provenance lib. | +| `known_edge` | java, typescript, csharp | Exactly one field resolved. Stronger than the call_edges tier of the same name: a field is not virtually dispatched, so this IS the storage location the access binds to. | +| `multi_inferred` | java, typescript, csharp | A sound SET: the receiver has more than one possible type, or two unrelated ancestors declare the name (which Java itself treats as ambiguous). Each member is one row. | +| `boundary_lib` | java, typescript, csharp | The field is declared in a staged library type. field_id is set and resolves in `fields` with provenance lib. | | `ambiguous_unknown` | java, typescript | Declared blind spot: the receiver could not be typed, or the name is not a member of the type it was typed to. field_id is NULL. Never dropped, and never replaced by a match on simple name. | **`field_access.field_provenance` values** | value | languages | meaning | |---|---|---| -| `client` | java, typescript | The field is declared in the analysed project. | -| `lib` | java, typescript | The field is declared in a staged library IR. | +| `client` | java, typescript, csharp | The field is declared in the analysed project. | +| `lib` | java, typescript, csharp | The field is declared in a staged library IR. | **Notes** -- **all** — JAVA AND TYPESCRIPT. The table is declared in every bundle and is EMPTY for Python, JavaScript and C# (C# fills `fields`, not `field_access`), so the schema does not churn as the remaining front ends land (#663). Check `SELECT count(*) FROM field_access` before reading an empty result as "nothing reads this field". +- **all** — JAVA, TYPESCRIPT AND C#. The table is declared in every bundle and is EMPTY for Python and JavaScript, so the schema does not churn as the remaining front ends land (#663). Check `SELECT count(*) FROM field_access` before reading an empty result as "nothing reads this field". +- **csharp** — ONLY RESOLVED SITES ARE ROWS: there is no ambiguous_unknown tier for C#. A C# member access is a field, a property, an event or a method group until resolution says which, so an unresolved one is not known to be a field access (#1445). A PROPERTY is not here either: reading it is a call, in call_edges with kind property_read or property_write. Enum members are not rows, because `fields` does not list them. - **typescript** — AN ACCESSOR IS NOT HERE. `get url()` read as `c.url` is a CALL, and call_edges already carries it with kind PROPERTY_READ or PROPERTY_WRITE (#703). The two tables are disjoint by construction: this one holds properties, call_edges holds accessors. Ask both when you want every read of a member. - **typescript** — AN ELEMENT ACCESS IS NOT HERE either: `obj["x"]` with a literal key is a different node kind and is not yet a site. A known gap, not a silent one. - **typescript** — A METHOD IS NOT A SITE. The callee of `obj.m()` is a PROPERTY_ACCESS node (37% of them, measured on one TypeScript library), and `const f = obj.m` reads a method as a value; neither is a data edge, and admitting them would fill the ambiguous_unknown tier with sites the engine HAS resolved elsewhere. Both are excluded and counted in ext_field_site_excluded with reasons method_callee and method_value. diff --git a/graph/bundle/build.ts b/graph/bundle/build.ts index c2a515f6..84c9fcb9 100644 --- a/graph/bundle/build.ts +++ b/graph/bundle/build.ts @@ -529,6 +529,28 @@ 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)) { + const row = sites.get(site as string); + if (row) fill(row, 3, nul(name as string)); + } + } // a site keyed on a type (Python METACLASS_CREATION) is positioned at the class declaration for (const row of sites.values()) { if (row[5] !== null) continue; 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 baee2954..e4ec00d9 100644 --- a/graph/bundle/languages.ts +++ b/graph/bundle/languages.ts @@ -149,6 +149,13 @@ export interface LanguageAdapter { * relation and absent from `fields`, so the join lost the row silently. */ configBinding?: RawSource; + /** + * (site, name), #1441. The accessor name of a site the IR gives no written name, where + * the engine knows it: a property read on a receiver with no type is a site with no + * target, and without the name a by-name lookup cannot offer it. Fills callee_name only + * where the IR left it NULL. + */ + siteNames?: RawSource; }; ir: { methods: MethodsIR; @@ -159,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; }; @@ -270,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' }, }, }; @@ -330,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. @@ -376,6 +393,11 @@ const CSHARP: LanguageAdapter = { dispatchCandidates: { file: 'dispatch-candidates.csv', columns: [0, 1, 2] }, // (prov, type) — no "how"; every row is a construction typeInstantiated: { file: 'resolution-type-instantiated.csv', columns: [1], constant: 'new' }, + // site, caller, field, fieldProvenance, tier, access: the Java shape (#1445). Only resolved + // sites have a row; a property is a call and stays in call edges through its accessor. + fieldAccess: { file: 'field-access.csv', columns: [0, 1, 2, 3, 4, 5] }, + // (site, recv, accessor name): an unresolved member access, get_X or set_X + siteNames: { file: 'site-unresolved-member.csv', columns: [0, 2] }, }, ir: { // A C# method row carries BOTH its module and its type, and the type is empty for a 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 87b31156..918e4a10 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -358,6 +358,10 @@ 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: 'using_dispose', languages: C, meaning: 'SYNTHESISED. The `Dispose()` a `using` statement or declaration runs when its scope ends. FromExpr is the resource expression.' }, + { table: 'call_edges', column: 'kind', value: 'foreach_enumerator', languages: C, meaning: 'SYNTHESISED. The `GetEnumerator()` a `foreach` runs on the iterated collection. FromExpr is the collection expression.' }, + { table: 'call_edges', column: 'kind', value: 'collection_add', languages: C, meaning: 'SYNTHESISED. The `Add(...)` a collection initializer (`new Bag { 1, { 2, 3 } }`) runs for each element. FromExpr is the element.' }, { 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.' }, @@ -384,6 +388,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'run', column: 'key', value: 'client_ir', languages: 'all', meaning: 'Path of the client IR directory the engine read.' }, { table: 'run', column: 'key', value: 'library_roots', languages: 'all', meaning: 'Comma-separated library IR roots staged as the type oracle; empty for a client-only run.' }, { table: 'run', column: 'key', value: 'dispatch_cap', languages: 'all', meaning: 'Fan-width cap on virtual dispatch in effect; `off` when uncapped.' }, + { table: 'run', column: 'key', value: 'dispatch_closed_world', languages: 'all', meaning: '`on` when the dispatch fan was narrowed to types the analysed code constructs (a closed-world premise; each dropped edge is an assumption row); `off` otherwise. Read by TypeScript.' }, { table: 'run', column: 'key', value: 'jdk_depth', languages: 'all', meaning: 'Platform-library hop cap (engine-ii).' }, { table: 'run', column: 'key', value: 'lib_depth', languages: 'all', meaning: 'External-library hop cap; `uncapped` when unset.' }, { table: 'run', column: 'key', value: 'engine_ii', languages: 'all', meaning: '`on` when the library-frontier forward chain (engine-ii) was included; `off` for a client-only solve.' }, @@ -513,6 +518,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'tier', value: 'fan_capped', languages: ['javascript', 'java', 'csharp'], meaning: 'More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count.' }, { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: ['javascript', 'typescript'], meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: S, meaning: '`x.emit(\'name\')` reaching a handler registered by `x.on(\'name\', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one.' }, + { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: T, meaning: 'A publish on a project class\'s keyed registry reaching each function filed under the same key: `bus.publish(\'x\', p)` where `publish` (or a method it passes the key to) calls what `this.handlers.get(name)` holds, to the function a `bus.subscribe(\'x\', f)` stored there, or an entry `{ [\'x\']: f }` of a table a subscribe-all method files by key. A literal, a const or a const-object member key; a key only known at run time matches every key, and a constant the dispatch also looks up (a wildcard) matches every publish. The site is the publish call (call-edge-generation/keyed_registry.dl).' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: J, meaning: 'A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e\'s static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity\'s type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl).' }, { table: 'call_edges', column: 'tier', value: 'intrinsic_terminal', languages: T, meaning: 'The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name.' }, @@ -522,15 +528,15 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'fields', column: 'provenance', value: 'client', languages: ['java', 'typescript'], meaning: 'Declared in the analysed project.' }, { table: 'fields', column: 'provenance', value: 'lib', languages: ['java', 'typescript'], meaning: 'Declared in a staged library IR.' }, { table: 'fields', column: 'provenance', value: 'generated', languages: J, meaning: 'Declared by a compile-time annotation processor and synthesised by the bundle, same shape and same reason as methods.provenance `generated`.' }, - { table: 'field_access', column: 'access', value: 'read', languages: ['java', 'typescript'], meaning: 'The value is used and not replaced.' }, - { table: 'field_access', column: 'access', value: 'write', languages: ['java', 'typescript'], meaning: 'The value is replaced without being read: a plain assignment `f = v`.' }, - { table: 'field_access', column: 'access', value: 'readwrite', languages: ['java', 'typescript'], meaning: 'The value is read and replaced at the one site: a compound assignment `f += v`, or `f++` / `--f`. One row, not two — a consumer asking "who writes f" and one asking "who reads f" must both match it.' }, - { table: 'field_access', column: 'tier', value: 'known_edge', languages: ['java', 'typescript'], meaning: 'Exactly one field resolved. Stronger than the call_edges tier of the same name: a field is not virtually dispatched, so this IS the storage location the access binds to.' }, - { table: 'field_access', column: 'tier', value: 'multi_inferred', languages: ['java', 'typescript'], meaning: 'A sound SET: the receiver has more than one possible type, or two unrelated ancestors declare the name (which Java itself treats as ambiguous). Each member is one row.' }, - { table: 'field_access', column: 'tier', value: 'boundary_lib', languages: ['java', 'typescript'], meaning: 'The field is declared in a staged library type. field_id is set and resolves in `fields` with provenance lib.' }, + { table: 'field_access', column: 'access', value: 'read', languages: ['java', 'typescript', 'csharp'], meaning: 'The value is used and not replaced.' }, + { table: 'field_access', column: 'access', value: 'write', languages: ['java', 'typescript', 'csharp'], meaning: 'The value is replaced without being read: a plain assignment `f = v`.' }, + { table: 'field_access', column: 'access', value: 'readwrite', languages: ['java', 'typescript', 'csharp'], meaning: 'The value is read and replaced at the one site: a compound assignment `f += v`, or `f++` / `--f`. One row, not two — a consumer asking "who writes f" and one asking "who reads f" must both match it.' }, + { table: 'field_access', column: 'tier', value: 'known_edge', languages: ['java', 'typescript', 'csharp'], meaning: 'Exactly one field resolved. Stronger than the call_edges tier of the same name: a field is not virtually dispatched, so this IS the storage location the access binds to.' }, + { table: 'field_access', column: 'tier', value: 'multi_inferred', languages: ['java', 'typescript', 'csharp'], meaning: 'A sound SET: the receiver has more than one possible type, or two unrelated ancestors declare the name (which Java itself treats as ambiguous). Each member is one row.' }, + { table: 'field_access', column: 'tier', value: 'boundary_lib', languages: ['java', 'typescript', 'csharp'], meaning: 'The field is declared in a staged library type. field_id is set and resolves in `fields` with provenance lib.' }, { table: 'field_access', column: 'tier', value: 'ambiguous_unknown', languages: ['java', 'typescript'], meaning: 'Declared blind spot: the receiver could not be typed, or the name is not a member of the type it was typed to. field_id is NULL. Never dropped, and never replaced by a match on simple name.' }, - { table: 'field_access', column: 'field_provenance', value: 'client', languages: ['java', 'typescript'], meaning: 'The field is declared in the analysed project.' }, - { table: 'field_access', column: 'field_provenance', value: 'lib', languages: ['java', 'typescript'], meaning: 'The field is declared in a staged library IR.' }, + { table: 'field_access', column: 'field_provenance', value: 'client', languages: ['java', 'typescript', 'csharp'], meaning: 'The field is declared in the analysed project.' }, + { table: 'field_access', column: 'field_provenance', value: 'lib', languages: ['java', 'typescript', 'csharp'], meaning: 'The field is declared in a staged library IR.' }, // type_use.* (#663) { table: 'type_use', column: 'tier', value: 'known_edge', languages: ['java', 'typescript'], meaning: 'Exactly one type. A type reference is not dispatched, so this IS the declaration the name denotes.' }, @@ -665,18 +671,22 @@ 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.' }, { table: 'entry_points', column: 'reason', value: 'test', languages: ['java', 'typescript', 'csharp'], meaning: 'Java: a JUnit test or lifecycle method. TypeScript: a function body handed to a test registrar (`it`, `describe`), inline or named, which the runner invokes. C#: an xUnit, NUnit or MSTest test method, or a set-up or tear-down hook of one.' }, { table: 'entry_points', column: 'reason', value: 'http', languages: ['java', 'python', 'typescript', 'csharp'], meaning: 'A route handler a web framework invokes on a request. Java: a JAX-RS / Spring MVC handler. Python: a function registered with a decorator naming an HTTP verb and a URL path. TypeScript: a handler passed to a route registration (`app.get(\'/x\', h)`), inline or named, or a method carrying a route decorator inside a container-owned class (`@Controller` + `@Get`). C#: a routed controller action, or a Razor Pages page model\'s `On[Handler][Async]` method.' }, - { table: 'entry_points', column: 'reason', value: 'cli', languages: J, meaning: 'A CLI command method (picocli etc.).' }, + { table: 'entry_points', column: 'reason', value: 'cli', languages: ['java', 'python'], meaning: 'A CLI command method. Java: picocli and the like. Python: the handler of a management command, a `Command` class in a `management/commands/` module, which the command runner calls by the module\'s file name.' }, { table: 'entry_points', column: 'reason', value: 'bean_ctor', languages: ['java', 'typescript'], meaning: 'Constructor of a container-managed class. TypeScript: the class carries a framework decorator (`@Injectable`, `@Component`, `@Module`), so the container constructs it and nothing in the repository does.' }, { table: 'entry_points', column: 'reason', value: 'factory', languages: J, meaning: 'A `@Bean` factory method.' }, - { table: 'entry_points', column: 'reason', value: 'lifecycle', languages: ['java', 'typescript', 'csharp'], meaning: 'Java: `@PostConstruct` / `@PreDestroy` and similar hooks. TypeScript: a hook the container calls by name on a decorated class (`ngOnInit`, `onModuleInit`), which has no call site anywhere. C#: a method the host calls on a hosted service (`ExecuteAsync`, `StartAsync`, `StopAsync`, and the `IHostedLifecycleService` hooks), including one that derives from the host\'s base through the project\'s own base class.' }, + { table: 'entry_points', column: 'reason', value: 'lifecycle', languages: ['java', 'typescript', 'csharp', 'python'], meaning: 'Python: a callable an application registers to run around requests, on an error or at start and stop: a middleware, an exception handler, a startup or shutdown handler, a lifespan. Java: `@PostConstruct` / `@PreDestroy` and similar hooks. TypeScript: a hook the container calls by name on a decorated class (`ngOnInit`, `onModuleInit`), which has no call site anywhere. C#: a method the host calls on a hosted service (`ExecuteAsync`, `StartAsync`, `StopAsync`, and the `IHostedLifecycleService` hooks), including one that derives from the host\'s base through the project\'s own base class.' }, { table: 'entry_points', column: 'reason', value: 'queue', languages: ['java', 'csharp'], meaning: 'A message-listener method. Java: also a Spring application event listener (`@EventListener`, `@TransactionalEventListener`, an `ApplicationListener` implementation). C#: a broker consumer or a bus message handler.' }, { table: 'entry_points', column: 'reason', value: 'scheduled', languages: J, meaning: 'A `@Scheduled` method.' }, { table: 'entry_points', column: 'reason', value: 'grpc_service', languages: ['java', 'python', 'csharp'], meaning: 'A gRPC service implementation the server invokes on a request, with no call site reaching it: a generated `ImplBase` override (Java); a class deriving from a generated `*Servicer` base in a `_pb2_grpc` module, overriding a method that base declares (Python).' }, @@ -697,8 +707,8 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'entry_points', column: 'reason', value: 'fixture', languages: P, meaning: 'A declared fixture that some collected test requests by parameter name. The runner calls it to build the argument.' }, { table: 'entry_points', column: 'reason', value: 'url', languages: P, meaning: 'A view named as a value in a module-level route table. The framework calls it on a request.' }, { table: 'entry_points', column: 'reason', value: 'signal_receiver', languages: P, meaning: 'A handler attached to a signal, by decorator or by connect(). It runs when the signal fires, whether or not this tree contains the send.' }, - { table: 'entry_points', column: 'reason', value: 'di_provider', languages: P, meaning: 'A provider named in a dependency-injection marker in a parameter default. The framework calls it and passes the result in.' }, - { table: 'entry_points', column: 'reason', value: 'orm_hook', languages: ['python', 'csharp'], meaning: 'A hook the data layer calls; nothing in the client does. Python: a lifecycle or validation hook registered by decoration. C#: an Entity Framework Core override or implementation: `OnModelCreating`, `OnConfiguring`, `IEntityTypeConfiguration.Configure`, a migration\'s `Up`/`Down`, a save-changes interceptor, `IDesignTimeDbContextFactory.CreateDbContext`.' }, + { table: 'entry_points', column: 'reason', value: 'di_provider', languages: P, meaning: 'A provider named in a dependency-injection marker: a parameter default, `Annotated` metadata (in place or through an alias), or a `dependencies=[...]` list on a route or router. A def, a class (its constructor) or a callable instance (its `__call__`). The framework calls it and passes the result in.' }, + { table: 'entry_points', column: 'reason', value: 'orm_hook', languages: ['python', 'csharp'], meaning: 'A hook the data layer calls; nothing in the client does. Python: a lifecycle, validation or serialization hook registered by decoration, or a `model_post_init` override on a model class. C#: an Entity Framework Core override or implementation: `OnModelCreating`, `OnConfiguring`, `IEntityTypeConfiguration.Configure`, a migration\'s `Up`/`Down`, a save-changes interceptor, `IDesignTimeDbContextFactory.CreateDbContext`.' }, { table: 'entry_points', column: 'reason', value: 'framework_hook', languages: C, meaning: 'A method a framework calls on a class because the class derives from one of its base types or implements one of its interfaces, directly or through the project\'s own bases: an options setup class, a view component, an authorization handler, a model binder, a gRPC interceptor, a FluentValidation validator\'s constructor and overrides, a MediatR request or notification handler, `Dispose`/`DisposeAsync` on an `IDisposable`/`IAsyncDisposable`.' }, { table: 'entry_points', column: 'reason', value: 'hub', languages: C, meaning: 'A public instance method of a SignalR hub. A connected client invokes it by name.' }, @@ -746,7 +756,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.' }, @@ -754,7 +764,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'python', table: 'entry_points', note: 'Framework entry points only: url, http, orm_hook, task, signal_receiver, fixture, di_provider and grpc_service. There is no test and no main reason: a pytest test is recognised by the query layer from its file and name, not here.' }, { language: 'python', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The Python dispatch envelope is in dispatch_candidates with basis `mro`, and `value` for a function assigned onto an instance\'s member; the raw linearisation is in ext_mro_position.' }, { language: 'python', table: 'type_instantiated', note: 'Every row has how = `new`: the rule set records that some client call constructs the class, not which form.' }, - { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, + { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, { language: 'java', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local\'s initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy.' }, { language: 'typescript', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too.' }, { language: 'python', table: 'call_edges', note: 'A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript.' }, @@ -764,8 +774,9 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'java', table: 'type_use', note: 'EVERY DEPTH is here, unlike the receiver-typing relations the engine uses internally, which filter to depth 0. A field of type `Map` produces three rows. Filter on `depth = 0` when you want the type an expression has rather than every type its declaration mentions.' }, { language: 'java', table: 'type_use', note: 'A TYPE_VARIABLE reference (`T`, `E`) is not a row: it names the declaration\'s own parameter, not a type. Where the parameter has a written bound the USE resolves to that bound and IS a row, so ` void f(T t)` records a use of Node.' }, { language: 'java', table: 'type_use', note: 'The reference rows carry no line in the Java IR (every all-type-references row has an empty startLine), so this table has no position columns. Use owner_method_id, or owner_type_id plus the types row, to locate a use.' }, - { language: 'all', table: 'field_access', note: 'JAVA AND TYPESCRIPT. The table is declared in every bundle and is EMPTY for Python, JavaScript and C# (C# fills `fields`, not `field_access`), so the schema does not churn as the remaining front ends land (#663). Check `SELECT count(*) FROM field_access` before reading an empty result as "nothing reads this field".' }, + { language: 'all', table: 'field_access', note: 'JAVA, TYPESCRIPT AND C#. The table is declared in every bundle and is EMPTY for Python and JavaScript, so the schema does not churn as the remaining front ends land (#663). Check `SELECT count(*) FROM field_access` before reading an empty result as "nothing reads this field".' }, { language: 'all', table: 'fields', note: 'JAVA, TYPESCRIPT AND C#. EMPTY for Python and JavaScript: a Python attribute is a symbols row of kind field. In C# it holds true fields only; a property is a symbols row of kind field with a property id, and its accessors are methods rows (PROPERTY_GET, PROPERTY_SET, PROPERTY_INIT).' }, + { language: 'csharp', table: 'field_access', note: 'ONLY RESOLVED SITES ARE ROWS: there is no ambiguous_unknown tier for C#. A C# member access is a field, a property, an event or a method group until resolution says which, so an unresolved one is not known to be a field access (#1445). A PROPERTY is not here either: reading it is a call, in call_edges with kind property_read or property_write. Enum members are not rows, because `fields` does not list them.' }, { language: 'typescript', table: 'field_access', note: 'AN ACCESSOR IS NOT HERE. `get url()` read as `c.url` is a CALL, and call_edges already carries it with kind PROPERTY_READ or PROPERTY_WRITE (#703). The two tables are disjoint by construction: this one holds properties, call_edges holds accessors. Ask both when you want every read of a member.' }, { language: 'typescript', table: 'field_access', note: 'AN ELEMENT ACCESS IS NOT HERE either: `obj["x"]` with a literal key is a different node kind and is not yet a site. A known gap, not a silent one.' }, { language: 'typescript', table: 'field_access', note: 'A METHOD IS NOT A SITE. The callee of `obj.m()` is a PROPERTY_ACCESS node (37% of them, measured on one TypeScript library), and `const f = obj.m` reads a method as a value; neither is a data edge, and admitting them would fill the ambiguous_unknown tier with sites the engine HAS resolved elsewhere. Both are excluded and counted in ext_field_site_excluded with reasons method_callee and method_value.' }, 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 fdd5d50a..61db248e 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -38,6 +38,10 @@ // site in process (a mediator's Send reaching the handler for the // request type). Added beside the site's row, by // framework-behavior/dispatch.dl. +// callback_registered NOT the site's own target either: a method group handed +// over as a value (an argument, or `+=` on an event the engine +// does not know), which the receiver of it may invoke. Added +// beside the call it is an argument of (resolution/delegates.dl). // ============================================================================ // ── THE SITE UNIVERSE ─────────────────────────────────────────────────────── @@ -96,8 +100,11 @@ call_target_count(prov, e, n) :- // A site whose receiver type has an INCOMPLETE HIERARCHY is never known_edge. The // member may well be declared in the base nobody staged, so the one target that // resolved is a target and so is whatever the missing base declares. Saying -// known_edge there claims monomorphism the engine cannot support. -site_leaves_client(prov, e) :- call_recv_type(prov, e, gk), type_hierarchy_incomplete(prov, gk). +// known_edge there claims monomorphism the engine cannot support. Only an entry +// member lookup can reach counts (type_members_incomplete): an unstaged interface +// after a class's base, or on a struct, declares nothing a class or struct receiver +// can bind to (#1431). +site_leaves_client(prov, e) :- call_recv_type(prov, e, gk), type_members_incomplete(prov, gk). site_leaves_client(prov, e) :- client_calls_lib(prov, e, _, _). // A site whose target is an UNSTAGED type leaves the client too. Without this every // `Console.WriteLine` in a client-only run is counted as an engine blind spot, and on @@ -108,6 +115,7 @@ site_leaves_client(prov, e) :- external_target(prov, e, _). // Invoke, which the runtime supplies (resolution/delegates.dl). The site gets an edge // to that member's Invoke node and the stored functions are in the envelope. site_leaves_client(prov, e) :- delegate_member_call(prov, e, _). +site_leaves_client("client", e) :- delegate_param_call(e, _). call_class(prov, e, "fan_capped") :- call_fan_refused(prov, e). call_class(prov, e, "known_edge") :- @@ -127,6 +135,17 @@ call_class(prov, e, "ambiguous_dynamic") :- call_class(prov, e, "known_implicit_ctor") :- call_target_count(prov, e, 0), call_ctor_implicit(prov, e, _). +// ...AND WHICH TYPE IT CONSTRUCTS. The edge above has no callee, truthfully: there is +// no constructor to point at. But the site still instantiates a type the engine +// knows, and without the type a consumer asking "who creates SystemClock" found +// nothing, while the same `new` of a class that writes a constructor is an edge to +// that constructor (#1473). One row per declaration of the type (each part of a +// partial class), keyed on the site, so it joins call_sites without touching the +// edge table. +ctor_implicit_type(e, t) :- + call_class("client", e, "known_implicit_ctor"), + call_ctor_implicit("client", e, gk), type_group("client", t, gk). + // AN OPERATOR OR CAST THAT RAN NO USER CODE. `i + 1`, `s == null`, `(int)d`: the // operator is one of the language's built-ins and the cast is a numeric or // reference conversion. There is no method anywhere -- System.Int32 declares no @@ -228,6 +247,31 @@ call_chain_edge(e, caller, "-", node, "client", "boundary_lib", kind) :- call_from("client", e, caller), invocation_site("client", e, kind). +// client -> A DELEGATE PARAMETER'S INVOKE: `act(1)` through a parameter the engine +// saw handed a function (resolution/delegate-params.dl). One node per parameter, the +// member's shape, and what it may run is in method_dispatch_candidate. +call_chain_edge(e, caller, "-", node, "client", "boundary_lib", kind) :- + delegate_param_call(e, p), + delegate_param_node(p, node), + call_from("client", e, caller), + invocation_site("client", e, kind). + +// ── HAND-OFF EDGES: A METHOD GROUP PASSED ON (#1550) ──────────────────────── +// `new Timer(Sweep, ...)`, `Run(Report)`, `ProcessExit += OnExit`: the enclosing +// method hands the method to code that may invoke it (resolution/delegates.dl, +// method_group_handoff). Tier `callback_registered`, the tier the JavaScript engine +// gives the same hand-off, so no reader takes it for a call written there; kind +// `ref`, Java's kind for `X::m`, the same construct. FromExpr is the method group +// itself, positioned where the name is written. +// +// Not an invocation_site, for the reason the primary-constructor edge below is not: +// the site universe is the parser's call list, and a method group is not a call. The +// call it is an argument of (`Run`, `new Timer`) keeps its own row and tier. +call_chain_edge(e, caller, "-", m, "client", "callback_registered", "ref") :- + method_group_handoff("client", e, m), + method_decl("client", _, _, _, m), + call_from_expr("client", e, caller). + // ── SYNTHESISED EDGES: A PRIMARY CONSTRUCTOR'S BASE INVOCATION ────────────── // `class D(int a) : B(a)` calls B's constructor with no call syntax anywhere in the // body. The kind says primary_ctor_base so a synthesised edge is never mistaken for @@ -252,6 +296,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 @@ -276,6 +366,14 @@ call_chain_edge(e, caller, "-", acc, "lib", "boundary_lib", kind) :- method_decl("lib", _, _, _, acc), call_from_expr("client", e, caller). +// client -> the bodiless accessor the site is written against (properties.dl), with +// the site's own tier, as call_names_bodiless does for a method. +call_chain_edge(e, caller, "-", decl, "client", tier, kind) :- + accessor_names_bodiless("client", e, decl, kind), + method_decl("client", _, _, _, decl), + call_from_expr("client", e, caller), + accessor_tier("client", e, tier). + // The tier of an accessor site, by the same rule as a call site: one target is // known_edge, several is a sound set. A property accessor can be virtual, so the fan // is real rather than theoretical. @@ -301,6 +399,7 @@ call_has_edge(prov, e) :- client_calls_client(prov, e, _, _). call_has_edge(prov, e) :- client_calls_lib(prov, e, _, _). call_has_edge("client", e) :- client_calls_external(e, _). call_has_edge("client", e) :- delegate_member_call("client", e, _). +call_has_edge("client", e) :- delegate_param_call(e, _). call_chain_edge(e, caller, "-", "-", "-", cls, kind) :- invocation_site("client", e, kind), @@ -329,6 +428,72 @@ call_chain_edge(e, caller, "-", label, "external", "boundary_lib", "indexer") :- external_element_access("client", e, label), call_from_expr("client", e, caller). +// A MEMBER ACCESS ON A RECEIVER THAT IS A SITE WITH NO TYPE. `options.Value.MaxCount` +// on an IOptions the run was not given: `options.Value` is labelled +// above, and nothing says what it returns, so `.MaxCount` has no member to look up +// and no type to name. It reached the output as no row at all, while the sibling +// `options.Value.Clamp()` is an ambiguous_unknown invocation, so impact dropped the +// reader from its answer and from its `bound:` (#1441). +// +// THE RECEIVER MUST BE A SITE, not merely an untyped expression. A namespace +// qualifier (`System.Text` in `System.Text.Encoding.UTF8`) is an untyped member +// access too, and it is not a call. A call, a property read or an indexer is one, and +// its result having no type is exactly the case this covers. Recursive, so every +// link of `a.B.C.D` past the first untyped one is its own site. +// +// No target. The accessor NAME is kept (get_ for a read or read-write, set_ for a +// write) in site_unresolved_member, which the bundle writes as the site's callee_name, +// so a by-name lookup offers the reader as a lead exactly as it offers a method call +// on the same receiver. +member_access_site_untyped(prov, q) :- + csite(prov, q), !expr_type(prov, q, _). +member_access_site_untyped(prov, q) :- + external_property_read(prov, q, _), !expr_type(prov, q, _). +member_access_site_untyped(prov, q) :- + external_element_access(prov, q, _), !expr_type(prov, q, _). +member_access_site_untyped(prov, q) :- + unresolved_member_access(prov, q, _, _). +// ...and through the syntax that passes a value on unchanged: `f()!.X`, `(f()).X`. +member_access_site_untyped(prov, e) :- + expr_kind(prov, e, "UNARY"), expr_operator(prov, e, "!"), + expr_child(prov, e, "UNARY_OPERAND", _, x), member_access_site_untyped(prov, x). +member_access_site_untyped(prov, e) :- + expr_kind(prov, e, "PARENTHESIZED"), !expr_type(prov, e, _), + expr_child(prov, e, "PARENTHESIZED_OPERAND", _, x), member_access_site_untyped(prov, x). + +unresolved_member_access(prov, acc, n, kind) :- + expr_kind(prov, acc, "MEMBER_ACCESS"), + !member_access_resolved(prov, acc), + !external_property_read(prov, acc, _), + !dynamic_property_read(prov, acc), + expr_qualifier_child(prov, acc, q), + member_access_site_untyped(prov, q), + expr_member_name_child(prov, acc, nameExpr), + expr_written_name(prov, nameExpr, n), + unresolved_member_access_kind(prov, acc, kind). +// A compound assignment (`x.N += 1`) is kept as the read: one site, one name. +unresolved_member_access_kind(prov, acc, "property_read") :- + expr_kind(prov, acc, "MEMBER_ACCESS"), !expr_is_write_target(prov, acc). +unresolved_member_access_kind(prov, acc, "property_write") :- + expr_kind(prov, acc, "MEMBER_ACCESS"), expr_is_write_target(prov, acc). + +// (site, receiver text, accessor name), in the shape of site_unresolved_named: the +// receiver text groups the failures, twenty reads off one untyped value being one gap. +unresolved_member_accessor(acc, cat("get_", n)) :- + unresolved_member_access("client", acc, n, "property_read"). +unresolved_member_accessor(acc, cat("set_", n)) :- + unresolved_member_access("client", acc, n, "property_write"). +site_unresolved_member(acc, recv, name) :- + unresolved_member_accessor(acc, name), + expr_qualifier_child("client", acc, q), expr_written_name("client", q, recv). +site_unresolved_member(acc, "", name) :- + unresolved_member_accessor(acc, name), + expr_qualifier_child("client", acc, q), !expr_written_name("client", q, _). + +call_chain_edge(acc, caller, "-", "-", "-", "ambiguous_unknown", kind) :- + unresolved_member_access("client", acc, _, kind), + call_from_expr("client", acc, caller). + // A PROPERTY READ THROUGH `dynamic`. The same declared blind spot as a call through // `dynamic`, and it reaches the output for the same reason: a site that produced no // row is indistinguishable from one the engine never saw. No target, because there @@ -385,6 +550,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/call-edge-generation/field_access.dl b/graph/csharp/engine/call-edge-generation/field_access.dl new file mode 100644 index 00000000..af939052 --- /dev/null +++ b/graph/csharp/engine/call-edge-generation/field_access.dl @@ -0,0 +1,105 @@ +// ============================================================================ +// CALL-EDGE-GEN · FIELD ACCESS AS A DATA EDGE (#1445, the C# half of #663) +// +// One row per (member access, resolved field), with the direction the data moves and +// how well the site resolved: the same six columns Java and TypeScript export, so the +// bundle reads one shape for every front end and `impact Type.Field` answers a C# +// field from the engine rather than from a name match. +// +// WHY IT WAS MISSING. A C# member access `x.Name` is resolved by the engine, not the +// parser (resolution/properties.dl says why: whether `Name` is a field, a property or +// an event is a resolution outcome). properties.dl turned the PROPERTY outcome into an +// accessor call and nothing turned the FIELD outcome into anything, so `_o.Limit` and +// `AuditOptions.SectionName` had no edge at all. A reader of another type's same-named +// field was then indistinguishable from a reader of this one. +// +// NOTHING NEW IS RESOLVED HERE. The joins are property_access's, with "field" in place +// of "property" in the member lookup: the qualifier typed by expr_type (an instance +// access) or named as a type by ref_names_type (a static field or a const), then +// member_lookup, which already carries C#'s hiding law and the client-to-library base +// chain. An unqualified name the parser bound itself (ref_denotes FIELD) is taken as +// the parser gave it, as property_access_unqualified takes a bare property. +// +// A PROPERTY IS NOT HERE. It is a call, and call_chain.dl emits it through its accessor. +// The two are disjoint by construction: member_lookup's "field" kind holds fields and +// consts, its "property" kind holds properties. An ENUM MEMBER is not here either: the +// bundle's `fields` table does not list C# enum members, so a row naming one would +// point at nothing. +// +// ONLY RESOLVED SITES HAVE A ROW. Java and TypeScript also emit an ambiguous_unknown row +// for a field-access site that did not resolve, because their parsers say which sites +// are field accesses. A C# MEMBER_ACCESS is a field, a property, an event, a method +// group or a namespace step until resolution says which, so an unresolved one is not +// known to be a field access at all, and a row for it would claim a blind spot on a +// field that may not exist. Unresolved member accesses stay counted where they already +// are (member_lookup_miss_real, external_property_read). +// +// TIERS: +// known_edge exactly one field. A field is not virtually dispatched, so this is +// the storage location the access binds to. +// multi_inferred 2+: the receiver typed to more than one type, and each named a field. +// boundary_lib the field is declared in a staged library type. +// ============================================================================ + +// ── RESOLUTION ────────────────────────────────────────────────────────────── +// `x.Name` on an instance: the qualifier's type declares (or inherits) a field `Name`. +// A member access that is the callee of an invocation is a method, never a field. +cs_field_access_target(prov, acc, f) :- + expr_kind(prov, acc, "MEMBER_ACCESS"), + !member_access_is_callee(prov, acc), + expr_qualifier_child(prov, acc, q), + expr_member_name_child(prov, acc, nameExpr), + expr_written_name(prov, nameExpr, n), + expr_type(prov, q, recvGk), + member_lookup(prov, recvGk, n, "field", f). + +// `Type.Name`: a static field or a const read through a type name. +cs_field_access_target(prov, acc, f) :- + expr_kind(prov, acc, "MEMBER_ACCESS"), + !member_access_is_callee(prov, acc), + expr_qualifier_child(prov, acc, q), + expr_member_name_child(prov, acc, nameExpr), + expr_written_name(prov, nameExpr, n), + ref_names_type(prov, q, gk), + member_lookup(prov, gk, n, "field", f). + +// `Name` unqualified, with an implicit `this` or the declaring type: the parser binds +// these itself. The MEMBER_NAME child of a member access is excluded: the access is +// the site, and counting its name child as well would store every qualified access +// twice. +cs_field_access_target(prov, e, f) :- + ref_denotes(prov, e, "FIELD", f), + !cs_member_name_expr(prov, e). +cs_member_name_expr(prov, e) :- expr_member_name_child(prov, _, e). + +// ── ACCESS DIRECTION ──────────────────────────────────────────────────────── +// properties.dl's write / read-write tests, reused so the two relations cannot disagree +// about whether a position writes: +// write a plain assignment or an object-initializer target; the old value is not read +// readwrite a compound assignment or `++` / `--` +// read everything else +cs_field_access_kind(prov, e, "write") :- cs_field_access_target(prov, e, _), expr_is_write_target(prov, e). +cs_field_access_kind(prov, e, "readwrite") :- cs_field_access_target(prov, e, _), expr_is_readwrite_target(prov, e), + !expr_is_write_target(prov, e). +cs_field_access_kind(prov, e, "read") :- cs_field_access_target(prov, e, _), + !expr_is_write_target(prov, e), !expr_is_readwrite_target(prov, e). + +// ── TIER ──────────────────────────────────────────────────────────────────── +cs_field_access_count(prov, e, c) :- cs_field_access_target(prov, e, _), + c = count : { cs_field_access_target(prov, e, _) }. +cs_field_access_class(prov, e, "known_edge") :- cs_field_access_count(prov, e, 1). +cs_field_access_class(prov, e, "multi_inferred") :- cs_field_access_count(prov, e, c), c >= 2. + +// ── THE EXPORTED ROW — field_access(Site, Caller, Field, Prov, Tier, Access) ─ +// The caller is call_from_expr's, the same attribution an accessor edge gets: the +// enclosing method, or the type for a field initializer. Client sites only, as every +// other exported edge. +field_access(e, caller, f, "client", cls, acc) :- + cs_field_access_target("client", e, f), field_decl("client", _, _, f), + call_from_expr("client", e, caller), + cs_field_access_class("client", e, cls), cs_field_access_kind("client", e, acc). +field_access(e, caller, f, "lib", "boundary_lib", acc) :- + cs_field_access_target("client", e, f), field_decl("lib", _, _, f), + !field_decl("client", _, _, f), + call_from_expr("client", e, caller), + cs_field_access_kind("client", e, acc). diff --git a/graph/csharp/engine/call-edge-generation/implicit-calls.dl b/graph/csharp/engine/call-edge-generation/implicit-calls.dl new file mode 100644 index 00000000..b81957a3 --- /dev/null +++ b/graph/csharp/engine/call-edge-generation/implicit-calls.dl @@ -0,0 +1,67 @@ +// ============================================================================ +// CALL-EDGE GEN · CALLS THE LANGUAGE WRITES FOR YOU +// +// Three statements run a method nobody names at the site: +// +// using (var r = new Res()) { } using var r = ...; using (r) { } +// -> r.Dispose() when the block ends +// foreach (var x in bag) { } +// -> bag.GetEnumerator() +// new Bag { 1, { 2, 3 } } +// -> Bag.Add(1), Bag.Add(2, 3) +// +// None of them has an INVOCATION in the IR, so no call site carried the edge and the +// method's body was unreachable from the code that depends on it: on one subject the +// Dispose of a reader that every test opens in a `using` had no caller at all. +// +// The method is the one member lookup finds on the STATIC type of the resource, the +// collection or the created object, by the name the language uses and the number of +// arguments it passes, then the ordinary dispatch fan. Only a member found in the +// project counts: a lookup that lands on a library interface (IDisposable.Dispose +// through an interface-typed resource) would fan to every implementor in view, which +// is the wide set this rule must not invent. +// ============================================================================ + +// ── THE THREE SHAPES, AS (site expression, receiver type, name, argument count) ── +// The site is the expression the call happens for: the resource, the iterated +// collection, the element of the initializer. It sits inside the method that runs the +// call, which is what call_from_expr reads. +implicit_call_shape(e, gk, "Dispose", "0", "using_dispose") :- + var_decl("client", _, "USING", _, v), var_initializer("client", v, e), expr_type("client", e, gk). +implicit_call_shape(e, gk, "Dispose", "0", "using_dispose") :- + expr_root_context("client", e, "USING_RESOURCE"), expr_role("client", e, "ROOT"), + !expr_parent("client", e, _, _, _), !using_var_initializer(e), expr_type("client", e, gk). +using_var_initializer(e) :- var_decl("client", _, "USING", _, v), var_initializer("client", v, e). +implicit_call_shape(e, gk, "GetEnumerator", "0", "foreach_enumerator") :- + var_iterated_initializer("client", _, e), expr_type("client", e, gk). +// A collection initializer: the INITIALIZER under an object creation whose elements +// are not member assignments. A nested `{ a, b }` element is one Add of two arguments. +implicit_call_shape(el, gk, "Add", "1", "collection_add") :- + collection_initializer(i, gk), expr_parent("client", el, i, _, _), + !expr_kind("client", el, "ASSIGNMENT"), !expr_kind("client", el, "INITIALIZER"). +implicit_call_shape(el, gk, "Add", n, "collection_add") :- + collection_initializer(i, gk), expr_parent("client", el, i, _, _), + expr_kind("client", el, "INITIALIZER"), + c = count : { expr_parent("client", _, el, _, _) }, n = to_string(c). +collection_initializer(i, gk) :- + expr_kind("client", oc, "OBJECT_CREATION"), expr_type("client", oc, gk), + expr_child("client", oc, "INITIALIZER_VALUE", _, i), expr_kind("client", i, "INITIALIZER"), + !initializer_has_assignment(i). +initializer_has_assignment(i) :- + expr_kind("client", i, "INITIALIZER"), expr_parent("client", el, i, _, _), expr_kind("client", el, "ASSIGNMENT"). + +// ── THE TARGETS ───────────────────────────────────────────────────────────── +implicit_call_target(e, t, kind) :- + implicit_call_shape(e, gk, n, pc, kind), + member_method("client", gk, n, m), method_decl("client", _, _, _, m), + method_paramc("client", m, pc), + dispatch_concrete_target("client", m, t), method_decl("client", _, _, _, t). +implicit_call_count(e, kind, c) :- + implicit_call_target(e, _, kind), c = count : { implicit_call_target(e, _, kind) }. + +call_chain_edge(e, caller, "-", t, "client", "known_edge", kind) :- + implicit_call_target(e, t, kind), implicit_call_count(e, kind, 1), + call_from_expr("client", e, caller). +call_chain_edge(e, caller, "-", t, "client", "multi_inferred", kind) :- + implicit_call_target(e, t, kind), implicit_call_count(e, kind, c), c >= 2, + call_from_expr("client", e, caller). 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 1814228c..72c20b46 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -96,6 +96,19 @@ expr_element_type(prov, e, gk) :- type_ref_module(prov, tr, mod), name_to_type(prov, mod, n, gk). +// (c') THE PROTOCOL THROUGH A FRAMEWORK ENUMERATOR. A collection in source whose +// GetEnumerator returns `IEnumerator` (an iterator, usually) yields +// MemberMap: the framework's Current is not staged, but the enumerator's one type +// argument is written on the method. +expr_element_type(prov, e, gk) :- + expr_type(prov, e, recvGk), + member_method(prov, recvGk, "GetEnumerator", getEnum), + method_return_type_ref(prov, getEnum, tr), + type_ref_name(prov, tr, en), (en = "IEnumerator" ; en = "IAsyncEnumerator"), + type_ref_argc(prov, tr, "1"), + type_ref_arg(prov, tr, "0", argRef), + type_ref_resolves(prov, argRef, gk). + // (d) THE RETURN OF A STAGED METHOD. `h.Items()[0]` where Items returns `Item[]` // from a library: the rank and the element name are on the library's own type // reference, so the clauses above -- which read the reference under the caller's @@ -274,6 +287,140 @@ expr_new_type(prov, e, gk) :- expr_type(prov, e, gk) :- expr_target_any(prov, e, m), method_return_type_group(prov, e2gk, gk), e2gk = m. +// ── ...AND WHERE THAT RETURN TYPE IS THE METHOD'S OWN TYPE PARAMETER ──────── +// `T Dep()` and `T Echo(T value)`: the return type is a reference to a type +// parameter, which resolves to no type on purpose (generics.dl), so the clause above +// types `_p.Dep()` as nothing and `.Spin()` on it was labelled external:T.Spin +// (#1549). The CALL says what T is, in one of two places, and both are the compiler's +// own answer rather than an approximation of it: +// +// EXPLICIT: the type argument written at the call, matched to the parameter by +// POSITION, as type_arg_for does for a type's parameters. +// INFERRED: with no type argument written, the static type of the argument passed +// to a parameter declared exactly `T`. An argument typed as a set gives the set. +// +// THE RETURN TYPE MUST BE `T` ITSELF. `T[]` and `List` HAVE the parameter and +// are not it; typing either as T would make the call's result its element. +// THE PARAMETER MUST BE THE METHOD'S. A method of a generic TYPE returning the +// type's `T` is #1448, answered by the receiver's type arguments, not the call's. +// NOT INFERRED FOR AN EXTENSION METHOD, whose receiver is parameter 0 and not an +// ARGUMENT child: every ordinal would be one off, and `Pick(this Foo f, T a, Bar b)` +// would take T from `b`. Nor through a named argument, a `params` parameter or a +// `T[]` parameter, where the ordinal or the element does not line up. +gm_return_param(prov, m, tp) :- + method_return_type_ref(prov, m, tr), type_ref_type_param(prov, tr, tp), + !type_ref_array_rank(prov, tr, _), + type_param_owner(prov, tp, m, "METHOD"). + +// A type argument written at the call: `Dep()`, root references only. +call_method_type_arg(prov, e, pos, tr) :- + type_ref_owner(prov, tr, e, "EXPRESSION"), type_ref_context(prov, tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent(prov, tr, _, _, _), + type_ref_root_position(prov, tr, pos). + +expr_type(prov, e, gk) :- + expr_target_any(prov, e, m), gm_return_param(prov, m, tp), + type_param_decl(prov, m, "METHOD", pos, _, tp), + call_method_type_arg(prov, e, pos, atr), type_ref_resolves(prov, atr, gk). +// A type argument that is itself a type parameter in scope, `Dep()` inside a +// method `where TW : Widget`, is typed by its constraint, as a value declared TW is. +expr_type(prov, e, gk) :- + expr_target_any(prov, e, m), gm_return_param(prov, m, tp), + type_param_decl(prov, m, "METHOD", pos, _, tp), + call_method_type_arg(prov, e, pos, atr), + type_ref_type_param(prov, atr, atp), type_param_bound(prov, atp, gk). + +// NOT INFERRED BESIDE A NON-GENERIC TWIN: `T Echo(T v)` and `Widget Echo(Gadget g)` +// in one type, called `Echo(new Gadget())`. Both are applicable with the same parameter +// types and C# picks the non-generic one, but overload selection may keep both, and +// the generic's inferred T would then give the call a second result type. The twin +// is found by name and parameter count on the declaration alone, so the guard is +// conservative: it gives up the inference rather than guessing which one wins. +gm_plain_twin(prov, m) :- + gm_return_param(prov, m, _), + method_in_type(prov, gk, m), method_name(prov, m, n), method_paramc(prov, m, pc), + method_in_type(prov, gk, m2), method_name(prov, m2, n), method_paramc(prov, m2, pc), + method_type_arity(prov, m2, "0"). + +// A parameter declared exactly the method's type parameter, by ordinal. +gm_infer_param(prov, m, k) :- + gm_return_param(prov, m, tp), + !method_is_extension(prov, m), !gm_plain_twin(prov, m), + param_decl(prov, m, pos, _, p), param_type_ref(prov, p, ptr), + type_ref_type_param(prov, ptr, tp), !type_ref_array_rank(prov, ptr, _), + !param_is_params(prov, p), + k = to_number(pos). + +// The argument's ORDINAL among the ARGUMENT children, as overload-args.dl counts it, +// at a site with no type argument written and no named argument. +gm_arg_at(prov, e, k, a) :- + call_type_argc(prov, e, "0"), !call_has_named_args(prov, e), + expr_argument(prov, e, pos, a), p = to_number(pos), + k = count : { expr_argument(prov, e, q, _), to_number(q) < p }. + +expr_type(prov, e, gk) :- + expr_target_any(prov, e, m), gm_infer_param(prov, m, k), + gm_arg_at(prov, e, k, a), expr_type(prov, a, gk). + +// ── ...AND A SERVICE LOCATOR, TYPED BY THE TYPE IT IS ASKED FOR ──────────── +// `sp.GetRequiredService()` returns an IOrderJob, and the knob table +// already says so (framework-behavior/knobs.dl, cs_di_resolve). The method itself is +// an extension in a package the run is rarely given, so it resolves to nothing and +// the clauses above never see its `T`: `job.Run()` on the result was a by-name lead +// and `sp.GetRequiredService().Start()` had no receiver type (#1471). +// Typed here from the ONE type argument written at the call, the same argument the +// explicit clause above reads for an in-source generic method, so a staged +// declaration of the locator, whose return is its own `T`, gives the same answer. +// NOT for a name the client declares itself: a project's own `GetService()` +// is typed by its own declaration, whatever it returns. The non-generic +// `GetService(typeof(T))` returns object and is typed by the cast around it. +expr_type(prov, e, gk) :- + call_callee_name(prov, e, n), cs_di_resolve(n), !cs_di_resolve_declared(n), + call_type_argc(prov, e, "1"), + call_method_type_arg(prov, e, "0", atr), type_ref_resolves(prov, atr, gk). +cs_di_resolve_declared(n) :- cs_di_resolve(n), method_name("client", _, n). + +// ── ...AND WHERE IT IS THE DECLARING TYPE'S PARAMETER ─────────────────────── +// `T Create()` on `interface IFactory`: here the RECEIVER says what T is, not the +// call. `IFactory f; f.Create().Count()` typed the call as the parameter, +// so `.Count()` was labelled external:T.Count (#1448), while `f.Current.Count()` +// through a property declared `T` resolved: member_type_substituted below does this +// for properties and fields, and this is the same substitution for a method's return. +// It is the shape of `IDbContextFactory.CreateDbContext()`. +// +// Each clause mirrors one of the property clauses below, so the two cannot disagree +// about which argument fills which parameter: the receiver's own reference +// (type_arg_for), a generic base the reference's type passed its parameter to +// (base_param_fill), and a base closed with a concrete argument (type_arg_fixed). +// As there, the return type must be `T` itself, not `T[]` or `List`, and the +// parameter is matched through its OWNER'S group rather than its spelling alone, so +// a derived type that renames the parameter substitutes nothing by coincidence. +decl_return_param(prov, m, ownerGk, pname) :- + method_return_type_ref(prov, m, tr), type_ref_type_param(prov, tr, tp), + !type_ref_array_rank(prov, tr, _), + type_param_owner(prov, tp, t, "TYPE"), type_param_name(prov, tp, pname), + type_group(prov, t, ownerGk). +// A staged generic type's method, called from the client: the declaration is the +// library's and the reference that supplies the argument is the client's, as for +// `IOptions.Value` below. +decl_return_param("client", m, ownerGk, pname) :- decl_return_param("lib", m, ownerGk, pname). + +expr_type(prov, e, argGk) :- + expr_target_any(prov, e, m), decl_return_param(prov, m, gk, pname), + call_receiver_expr(prov, e, r), expr_type_via_ref(prov, r, tr), + type_ref_resolves(prov, tr, gk), + type_arg_for(prov, tr, pname, argGk). +expr_type(prov, e, argGk) :- + expr_target_any(prov, e, m), decl_return_param(prov, m, baseGk, basePname), + call_receiver_expr(prov, e, r), expr_type_via_ref(prov, r, tr), + type_ref_resolves(prov, tr, gk), type_group(prov, derivedT, gk), + base_param_fill(prov, derivedT, baseGk, basePname, derivedPname), + type_arg_for(prov, tr, derivedPname, argGk). +expr_type(prov, e, argGk) :- + expr_target_any(prov, e, m), decl_return_param(prov, m, baseGk, basePname), + call_recv_type(prov, e, recvGk), + type_arg_fixed(prov, recvGk, baseGk, basePname, argGk). + // A LIBRARY METHOD'S RETURN TYPE, READ FROM A CLIENT SITE. Resolved in the // LIBRARY's scope and then mirrored, rather than resolved in the client's: // `string` inside a library file means what that file's scope says it means, and @@ -325,6 +472,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/expression-resolution/overload-args.dl b/graph/csharp/engine/expression-resolution/overload-args.dl index 6489ae95..7c65128b 100644 --- a/graph/csharp/engine/expression-resolution/overload-args.dl +++ b/graph/csharp/engine/expression-resolution/overload-args.dl @@ -163,6 +163,123 @@ ovl_rejected(prov, e, m) :- ovl_arg_type(prov, e, k, at), ovl_param_type(m, k, pt), !ovl_conv(at, pt). +// ── 1b. NOT APPLICABLE, BY SHAPE ──────────────────────────────────────────── +// Three more readings the arguments make certain, each tested against the parameter +// the argument lands on (an extension method called on a receiver shifted by one): +// +// a LAMBDA converts only to a delegate (or an expression of one): never to a +// predefined type, and to Func<..> / Action<..> / Expression<..> only +// with the delegate's parameter count. `Must(x => ..)` against +// Must(Func) / Must(Func) keeps the first. +// a PREDEFINED value (a literal, or a name declared with a predefined type) never +// converts to a delegate, to a non-`params` array, or to a type in +// source that declares no conversion operator, nor to an exception. +// a SOURCE-TYPED argument (its type a class or struct in source) never converts to a +// delegate, to a predefined type, or to another source type it neither +// derives from nor implements, when neither declares a conversion. +// +// Prune-only, and the site is never emptied (ovl_survives below). +ovl_ext_receiver(prov, e) :- call_receiver_expr(prov, e, r), !ref_names_type(prov, r, _). +ovl_param_at(prov, e, m, k, p) :- + ovl_site(prov, e), ovl_cand(prov, e, m), !method_is_extension(_, m), + param_decl(_, m, pos, _, p), k = to_number(pos), param_mode(_, p, "VALUE"), !param_is_params(_, p). +ovl_param_at(prov, e, m, k, p) :- + ovl_site(prov, e), ovl_cand(prov, e, m), method_is_extension(_, m), ovl_ext_receiver(prov, e), + param_decl(_, m, pos, _, p), k = to_number(pos) - 1, k >= 0, param_mode(_, p, "VALUE"), !param_is_params(_, p). +ovl_param_at(prov, e, m, k, p) :- + ovl_site(prov, e), ovl_cand(prov, e, m), method_is_extension(_, m), !ovl_ext_receiver(prov, e), + param_decl(_, m, pos, _, p), k = to_number(pos), param_mode(_, p, "VALUE"), !param_is_params(_, p). + +ovl_delegate_name("Func"). ovl_delegate_name("Action"). ovl_delegate_name("Predicate"). +ovl_delegate_name("Expression"). ovl_delegate_name("Comparison"). ovl_delegate_name("Converter"). +// the delegate a parameter's type names, and how many parameters it takes +ovl_param_delegate(p, tr) :- param_type_ref(_, p, tr), type_ref_name(_, tr, n), ovl_delegate_name(n), n != "Expression". +ovl_param_delegate(p, ar) :- param_type_ref(_, p, tr), type_ref_name(_, tr, "Expression"), type_ref_arg(_, tr, "0", ar). +ovl_delegate_arity(tr, n) :- type_ref_name(_, tr, "Func"), type_ref_argc(_, tr, c), n = to_number(c) - 1. +ovl_delegate_arity(tr, n) :- type_ref_name(_, tr, "Action"), type_ref_argc(_, tr, c), n = to_number(c). +ovl_delegate_arity(tr, 1) :- type_ref_name(_, tr, "Predicate"). +ovl_delegate_arity(tr, 2) :- type_ref_name(_, tr, "Comparison"). +ovl_delegate_arity(tr, 1) :- type_ref_name(_, tr, "Converter"). +ovl_param_is_delegate(p) :- param_type_ref(_, p, tr), type_ref_name(_, tr, n), ovl_delegate_name(n). +ovl_param_is_delegate(p) :- param_type_ref(_, p, tr), type_ref_resolves(_, tr, gk), type_group(_, t, gk), type_decl(_, _, _, _, "DELEGATE", t). + +ovl_lambda_arg(prov, e, k, n) :- + ovl_arg_at(prov, e, k, a), expr_anon_decl(prov, a, lam), + n = count : { param_decl(prov, lam, _, _, _) }. +ovl_rejected(prov, e, m) :- + ovl_lambda_arg(prov, e, k, n), ovl_param_at(prov, e, m, k, p), + ovl_param_delegate(p, tr), ovl_delegate_arity(tr, d), d != n. +ovl_rejected(prov, e, m) :- + ovl_lambda_arg(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_name(_, p, _, ctn), ovl_predef(ctn, _). + +// a parameter type no predefined value converts to +ovl_param_closed(p) :- ovl_param_is_delegate(p). +ovl_param_closed(p) :- param_type_name(_, p, _, ctn), strlen(ctn) > 2, substr(ctn, max(0, strlen(ctn) - 2), 2) = "[]". +ovl_param_closed(p) :- param_type_name(_, p, tn, _), strlen(tn) >= 9, substr(tn, max(0, strlen(tn) - 9), 9) = "Exception". +ovl_param_closed(p) :- param_type_ref(_, p, tr), type_ref_resolves(_, tr, gk), ovl_source_closed(gk). +// a class or struct in source with no conversion operator of its own +ovl_source_closed(gk) :- + type_group("client", t, gk), type_decl("client", _, _, _, tk, t), (tk = "CLASS" ; tk = "STRUCT" ; tk = "RECORD"), + !ovl_type_converts(gk). +ovl_type_converts(gk) :- method_in_type(_, gk, m), method_kind(_, m, "CONVERSION_OPERATOR"). +ovl_rejected(prov, e, m) :- + ovl_arg_type(prov, e, k, _), ovl_param_at(prov, e, m, k, p), ovl_param_closed(p). + +// a source-typed argument +ovl_arg_source(prov, e, k, gk) :- + ovl_arg_at(prov, e, k, a), !expr_anon_decl(prov, a, _), expr_type(prov, a, gk), ovl_source_closed(gk). +ovl_rejected(prov, e, m) :- + ovl_arg_source(prov, e, k, _), ovl_param_at(prov, e, m, k, p), ovl_param_is_delegate(p). +ovl_rejected(prov, e, m) :- + ovl_arg_source(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_name(_, p, _, ctn), ovl_predef(ctn, _). +ovl_rejected(prov, e, m) :- + ovl_arg_source(prov, e, k, gk), ovl_param_at(prov, e, m, k, p), + param_type_ref(_, p, tr), type_ref_resolves(_, tr, pgk), type_group("client", _, pgk), + !type_self_or_ancestor(prov, gk, pgk), !type_satisfies(prov, gk, pgk), !ovl_type_converts(pgk). + +// An INTERFACE-typed argument converts only to that interface, one it extends, or +// object: never to a class or struct in source, a delegate or a predefined type. +ovl_arg_iface(prov, e, k, gk) :- + ovl_arg_at(prov, e, k, a), !expr_anon_decl(prov, a, _), expr_type(prov, a, gk), + type_group("client", t, gk), type_is_interface("client", t). +ovl_rejected(prov, e, m) :- + ovl_arg_iface(prov, e, k, _), ovl_param_at(prov, e, m, k, p), ovl_param_is_delegate(p). +ovl_rejected(prov, e, m) :- + ovl_arg_iface(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_name(_, p, _, ctn), ovl_predef(ctn, _). +ovl_rejected(prov, e, m) :- + ovl_arg_iface(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_ref(_, p, tr), type_ref_resolves(_, tr, pgk), ovl_source_closed(pgk). +ovl_rejected(prov, e, m) :- + ovl_arg_iface(prov, e, k, gk), ovl_param_at(prov, e, m, k, p), + param_type_ref(_, p, tr), type_ref_resolves(_, tr, pgk), pgk != gk, + type_group("client", pt, pgk), type_is_interface("client", pt), + !type_self_or_ancestor(prov, gk, pgk), !type_satisfies(prov, gk, pgk), !interface_extends_tc(prov, gk, pgk). +interface_extends_tc(prov, a, b) :- interface_extends(prov, a, b). +interface_extends_tc(prov, a, c) :- interface_extends(prov, a, b), interface_extends_tc(prov, b, c). + +// A LAMBDA never converts to a class, struct, interface or enum declared in source, to +// an array, or to the reflection types an overload commonly pairs it with +// (`Map(m => m.Id)` against `Map(Type, MemberInfo)`). +ovl_rejected(prov, e, m) :- + ovl_lambda_arg(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_ref(_, p, tr), type_ref_resolves(_, tr, pgk), + type_group("client", t, pgk), !type_is_delegate("client", t). +ovl_rejected(prov, e, m) :- + ovl_lambda_arg(prov, e, k, _), ovl_param_at(prov, e, m, k, p), + param_type_name(_, p, tn, ctn), (ovl_reflection_type(tn) ; (strlen(ctn) > 2, substr(ctn, max(0, strlen(ctn) - 2), 2) = "[]")). +ovl_reflection_type("Type"). ovl_reflection_type("MemberInfo"). ovl_reflection_type("PropertyInfo"). +ovl_reflection_type("FieldInfo"). ovl_reflection_type("MethodInfo"). ovl_reflection_type("ParameterInfo"). +ovl_reflection_type("ConstructorInfo"). + +// EXPLICIT TYPE ARGUMENTS fix the generic arity: `GetField(0)` is one of the +// one-type-parameter overloads, never GetField(int) or GetField(int). +ovl_rejected(prov, e, m) :- + ovl_cand(prov, e, m), call_type_argc(prov, e, n), n != "0", n != "", + method_type_arity(_, m, a), a != n, !method_is_extension(_, m). + // ── 2. EXACT MATCH ────────────────────────────────────────────────────────── // Every argument identical to its parameter, as many parameters as arguments (no // default filled in, no params expansion), and not generic (a type parameter that diff --git a/graph/csharp/engine/framework-behavior/destinations.dl b/graph/csharp/engine/framework-behavior/destinations.dl index 4c7a0e87..06ada5b9 100644 --- a/graph/csharp/engine/framework-behavior/destinations.dl +++ b/graph/csharp/engine/framework-behavior/destinations.dl @@ -48,9 +48,7 @@ call_recv_written_name(e, n) :- // …and a property read off another value, `services.Mediator.Send(..)`: a minimal-API // endpoint takes its services as one parameter object, and the receiver is that // object's property, whose declared type name is what the project wrote. -call_recv_written_name(e, n) :- - call_receiver_expr("client", e, r), property_access("client", r, p, _), - property_type_name("client", p, n, _), n != "". +// (recv_value_written_name below) // …and a `var` from the service locator, which holds its type argument call_recv_written_name(e, n) :- call_receiver_expr("client", e, r), @@ -59,6 +57,47 @@ call_recv_written_name(e, n) :- type_ref_owner("client", tr, init, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), !type_ref_parent("client", tr, _, _, _), type_ref_name("client", tr, n). +// The same shapes with no local in between, and a local holding a member read. Each names +// the type the project WROTE at the value's source: +// `new Pricing.PricingClient(ch).Quote(..)` the created type +// `factory.CreateClient("o").Place(..)` the type argument of a +// service locator or client factory +// `Shared.Client.GetAsync(..)`, `Shared.Field.GetAsync(..)` a property's or field's declared type +// `var c = Shared.Client; c.GetAsync(..)` the same, through a `var` +// A client factory is its own knob (cs_client_factory_generic), not cs_di_resolve, which +// also names messaging receivers. +recv_value_written_name(x, n) :- expr_new_type_ref("client", x, tr), type_ref_name("client", tr, n). +recv_value_written_name(x, n) :- csite("client", x), call_callee_name("client", x, f), cs_di_resolve(f), + type_ref_owner("client", tr, x, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_name("client", tr, n). +recv_value_written_name(x, n) :- csite("client", x), call_callee_name("client", x, f), cs_client_factory_generic(f), + type_ref_owner("client", tr, x, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_name("client", tr, n). +recv_value_written_name(x, n) :- property_access("client", x, p, _), property_type_name("client", p, n, _), n != "". +recv_value_written_name(x, n) :- expr_kind("client", x, "MEMBER_ACCESS"), + expr_qualifier_child("client", x, q), expr_member_name_child("client", x, nm), expr_written_name("client", nm, fn), + ref_names_type("client", q, gk), member_lookup("client", gk, fn, "field", f), + field_type_name("client", f, n, _), n != "". +recv_value_written_name(x, n) :- expr_kind("client", x, "MEMBER_ACCESS"), + expr_qualifier_child("client", x, q), expr_member_name_child("client", x, nm), expr_written_name("client", nm, fn), + expr_type("client", q, gk), member_lookup("client", gk, fn, "field", f), + field_type_name("client", f, n, _), n != "". +call_recv_written_name(e, n) :- call_receiver_expr("client", e, r), recv_value_written_name(r, n). +call_recv_written_name(e, n) :- + call_receiver_expr("client", e, r), + ref_denotes("client", r, "LOCAL_VARIABLE", v), var_is_implicit("client", v), + var_value_initializer("client", v, init), recv_value_written_name(init, n). +call_recv_written_name(e, n) :- + call_receiver_expr("client", e, r), + ref_denotes("client", r, "LOCAL_VARIABLE", v), var_is_implicit("client", v), + var_value_initializer("client", v, init), ref_denotes("client", init, "PROPERTY", p), + property_type_name("client", p, n, _), n != "". +call_recv_written_name(e, n) :- + call_receiver_expr("client", e, r), + ref_denotes("client", r, "LOCAL_VARIABLE", v), var_is_implicit("client", v), + var_value_initializer("client", v, init), ref_denotes("client", init, "FIELD", f), + field_type_name("client", f, n, _), n != "". + // ───────────────────────────────────────────────────────────────────────────── // 2. gRPC // ───────────────────────────────────────────────────────────────────────────── @@ -97,12 +136,22 @@ grpc_service_of_name(n, svc, sfx) :- grpc_name_segs(n, svc, tail), svc != "", // ── the server end ────────────────────────────────────────────────────────── // A method a service class OVERRIDES from `.Base` serves `/`. Only an // override: every rpc is a virtual on the generated base, and a helper written beside -// the rpcs is not one. -grpc_serves(m, d) :- - heritage_slot("client", t, _, bn, _, _), - cs_grpc_server_suffix(sfx), grpc_service_of_name(bn, svc, sfx), - method_owner("client", t, m), method_modifier("client", m, "override"), - method_name("client", m, n), d = cat(svc, cat("/", n)). +// the rpcs is not one. The base is written `.Base` (optionally qualified), or +// `Base` alone after `using static .;`, and the service class may reach it +// through the project's own bases (`StockService : AuditedStockBase : Stock.StockBase`). +// grpc_base_at(T0, Svc): the type whose heritage WRITES the generated base. +grpc_base_at(t0, svc) :- heritage_slot("client", t0, _, bn, _, _), + cs_grpc_server_suffix(sfx), grpc_service_of_name(bn, svc, sfx). +grpc_base_at(t0, svc) :- grpc_static_base(t0, svc, _). +grpc_static_base(t0, svc, st) :- heritage_slot("client", t0, _, bn, _, _), !contains(".", bn), + type_module("client", t0, mod), using_static_type("client", mod, st), last_segment(st, svc), + cs_grpc_server_suffix(sfx), bn = cat(svc, sfx). +last_segment_demand(st) :- using_static_type("client", _, st). +grpc_service_type(gk, t0, svc) :- grpc_base_at(t0, svc), type_group("client", t0, gk). +grpc_service_type(gk, t0, svc) :- type_ancestor("client", gk, agk), grpc_base_at(t0, svc), type_group("client", t0, agk). +grpc_serves_at(m, d, t0) :- grpc_service_type(gk, t0, svc), method_in_type("client", gk, m), + method_modifier("client", m, "override"), method_name("client", m, n), d = cat(svc, cat("/", n)). +grpc_serves(m, d) :- grpc_serves_at(m, d, _). // ── the client end ────────────────────────────────────────────────────────── // A call on a receiver written `.Client`. The call name is the rpc, or the rpc @@ -129,9 +178,73 @@ grpc_send_primary(e, from, d) :- grpc_send_site(e, from, svc, n), grpc_name_ends_async(n) :- grpc_send_site(_, _, _, n), cs_grpc_async_suffix(a), strlen(n) > strlen(a), substr(n, strlen(n) - strlen(a), strlen(a)) = a. +// ── which namespace each end's `` can be in ────────────────────────────── +// The generated `` is never in source, so each end is read for the namespaces its +// written name could resolve in: a qualifier it wrote (`Widgets.Rpc.Catalog.CatalogBase`, +// relative to each enclosing namespace too), the namespace a `using static` names, or, +// unqualified, the enclosing namespaces and the file's and the project's `using`s. Two +// services called Catalog in two namespaces are then told apart by whether the client's +// namespaces and the server's meet. Framework namespaces (System, Microsoft) and the +// gRPC runtime's own never hold a generated service and are not evidence. +grpc_ns_demand(ns) :- grpc_base_at(t0, _), type_namespace("client", t0, ns). +grpc_ns_demand(ns) :- grpc_send_name(e, _), grpc_send_ns_home(e, ns). +grpc_ns_dot(ns, i) :- grpc_ns_demand(ns), i = range(0, strlen(ns)), substr(ns, i, 1) = ".". +grpc_ns_prefix(ns, ns) :- grpc_ns_demand(ns), ns != "". +grpc_ns_prefix(ns, p) :- grpc_ns_dot(ns, i), p = substr(ns, 0, i). +grpc_name_prefix(n, q) :- grpc_name_prev_dot(n, p), q = substr(n, 0, p). +grpc_global_using(ns) :- using_decl("client", _, ns, _, u), using_is_global("client", u), + !using_is_static("client", u), !using_is_alias("client", u). + +// the server: T0 is the type whose heritage names the base +grpc_written_base(t0, bn) :- heritage_slot("client", t0, _, bn, _, _), cs_grpc_server_suffix(sfx), + grpc_service_of_name(bn, _, sfx). +grpc_srv_ns(t0, q) :- grpc_written_base(t0, bn), grpc_name_prefix(bn, q). +grpc_srv_ns(t0, cat(p, cat(".", q))) :- grpc_written_base(t0, bn), grpc_name_prefix(bn, q), + type_namespace("client", t0, ns), grpc_ns_prefix(ns, p). +grpc_srv_ns(t0, p) :- grpc_written_base(t0, bn), !grpc_name_has_prev(bn), + type_namespace("client", t0, ns), grpc_ns_prefix(ns, p). +grpc_srv_ns(t0, u) :- grpc_written_base(t0, bn), !grpc_name_has_prev(bn), + type_module("client", t0, mod), using_namespace("client", mod, u). +grpc_srv_ns(t0, u) :- grpc_written_base(t0, bn), !grpc_name_has_prev(bn), grpc_global_using(u). +grpc_srv_ns(t0, q) :- grpc_static_base(t0, _, st), grpc_name_last_dot(st, l), q = substr(st, 0, l). +grpc_name_demand(st) :- using_static_type("client", _, st), contains(".", st). + +// the client: the name its receiver wrote, and the namespace the calling method is in +grpc_send_name(e, rn) :- call_recv_written_name(e, rn), cs_grpc_client_suffix(sfx), + grpc_service_of_name(rn, _, sfx), csite("client", e). +grpc_send_ns_home(e, ns) :- grpc_send_name(e, _), call_from("client", e, from), + method_owner("client", t, from), type_namespace("client", t, ns). +grpc_cli_ns(e, q) :- grpc_send_name(e, rn), grpc_name_prefix(rn, q). +grpc_cli_ns(e, cat(p, cat(".", q))) :- grpc_send_name(e, rn), grpc_name_prefix(rn, q), + grpc_send_ns_home(e, ns), grpc_ns_prefix(ns, p). +grpc_cli_ns(e, p) :- grpc_send_name(e, rn), !grpc_name_has_prev(rn), grpc_send_ns_home(e, ns), grpc_ns_prefix(ns, p). +grpc_cli_ns(e, u) :- grpc_send_name(e, rn), !grpc_name_has_prev(rn), call_module("client", e, mod), + using_namespace("client", mod, u). +grpc_cli_ns(e, u) :- grpc_send_name(e, rn), !grpc_name_has_prev(rn), grpc_global_using(u). +grpc_ns_not_evidence(ns) :- grpc_cli_ns(_, ns), cs_grpc_runtime_namespace(ns). +grpc_ns_not_evidence(ns) :- grpc_cli_ns(_, ns), cs_framework_namespace_root(r), ns = r. +grpc_ns_not_evidence(ns) :- grpc_cli_ns(_, ns), cs_framework_namespace_root(r), + strlen(ns) > strlen(r), substr(ns, 0, strlen(r) + 1) = cat(r, "."). + +// ── the join ──────────────────────────────────────────────────────────────── +// One service of that name: [exact], as before. Several: those whose namespaces meet the +// client's stay [exact] and the others are dropped; when none meets (the namespace +// cannot be read), every one is kept, one rung down, at `service_name`. +grpc_pair(e, from, to, d, t0) :- grpc_send_cand(e, from, d), grpc_serves_at(to, d, t0), from != to. +grpc_pair_t0(e, d, t0) :- grpc_pair(e, _, _, d, t0). +grpc_pair_n(e, d, k) :- grpc_pair_t0(e, d, _), k = count : { grpc_pair_t0(e, d, _) }. +grpc_pair_scoped(e, d, t0) :- grpc_pair_t0(e, d, t0), grpc_cli_ns(e, ns), !grpc_ns_not_evidence(ns), + grpc_srv_ns(t0, ns). +grpc_has_scoped(e, d) :- grpc_pair_scoped(e, d, _). +remote_edge_via(e, from, to, tr, d, conf) :- + cs_remote_transport_grpc(tr), cs_remote_confidence_exact(conf), + grpc_pair(e, from, to, d, t0), grpc_pair_n(e, d, 1). remote_edge_via(e, from, to, tr, d, conf) :- cs_remote_transport_grpc(tr), cs_remote_confidence_exact(conf), - grpc_send_cand(e, from, d), grpc_serves(to, d), from != to. + grpc_pair(e, from, to, d, t0), grpc_pair_n(e, d, k), k > 1, grpc_pair_scoped(e, d, t0). +remote_edge_via(e, from, to, tr, d, conf) :- + cs_remote_transport_grpc(tr), cs_remote_confidence_service_name(conf), + grpc_pair(e, from, to, d, _), grpc_pair_n(e, d, k), k > 1, !grpc_has_scoped(e, d). remote_send_at(e, from, tr, d) :- cs_remote_transport_grpc(tr), grpc_send_primary(e, from, d). serves_destination(m, tr, d) :- cs_remote_transport_grpc(tr), grpc_serves(m, d). @@ -229,17 +342,147 @@ http_route_raw(m, p2, verb) :- ctl_route_c(m, p, verb), tok_at_a(p, i), method_n p2 = cat(substr(p, 0, i), cat(n, substr(p, i + 8, strlen(p) - i - 8))). http_route_raw(m, p, verb) :- ctl_route_c(m, p, verb), !contains("[action]", p). +// ── the server: MVC controllers and their actions ─────────────────────────── +// An action is a public instance method of a controller: a non-abstract class named +// `Controller` or deriving from Controller/ControllerBase, not [NonAction], not an +// override of the framework's own virtuals. +mvc_ctl(t, c) :- type_decl("client", tn, _, _, "CLASS", t), !type_modifier("client", t, "abstract"), + !type_modifier("client", t, "static"), cs_http_controller_suffix(sfx), ends_with_suffix(tn, sfx), + c = substr(tn, 0, strlen(tn) - strlen(sfx)). +mvc_ctl(t, tn) :- type_decl("client", tn, _, _, "CLASS", t), !type_modifier("client", t, "abstract"), + !type_modifier("client", t, "static"), cs_http_controller_suffix(sfx), !ends_with_suffix(tn, sfx), + heritage_slot("client", t, _, bn, _, _), last_segment(bn, b), cs_http_controller_base(b). +mvc_non_action(m) :- attr_on(m, "METHOD", n, _), cs_http_non_action_attr(n). +mvc_action(t, m) :- mvc_ctl(t, _), method_owner("client", t, m), method_kind("client", m, "METHOD"), + member_access("client", m, "PUBLIC"), !method_is_static("client", m), + !method_modifier("client", m, "override"), !mvc_non_action(m). + +// Attribute routing, the class's route with no route on the action: every action of a +// `[Route("[controller]/[action]")]` class is served there, for any verb. Only when the +// class template names [action], so each action has a route of its own; a public method +// under a template without it would share its sibling's route, and is more often a +// helper someone forgot to mark [NonAction] than a route anyone calls. +ctl_action(m, "", verb) :- mvc_action(t, m), ctl_base(t, b), contains("[action]", b), + !method_has_verb(m), !method_has_route_tmpl(m), cs_http_verb_any(verb). + +// ── the server: conventional routes ───────────────────────────────────────── +// `app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}")` routes +// every action that has no attribute route: its class has no [Route], it has no template +// of its own, and it is not in an [ApiController] (which requires attribute routing). +// The route is the pattern with {controller} and {action} (and {area}) filled in; a verb +// attribute with no template narrows the verb. A pattern that does not spell +// {controller} or {action} takes it from the call's defaults, `new { controller = "X" }`. +conv_route_call(e, k) :- call_callee_name("client", e, n), cs_http_conventional_route(n, k), csite("client", e). +conv_pattern(e, p) :- conv_route_call(e, k), http_arg_string(e, k, p). +conv_pattern(e, p) :- call_callee_name("client", e, n), cs_http_default_route(n, p), csite("client", e). +conv_pattern(e, p) :- call_callee_name("client", e, n), cs_http_area_route(n, _, k), csite("client", e), + http_arg_string(e, k, p). +conv_area_fixed(e, a) :- call_callee_name("client", e, n), cs_http_area_route(n, k, _), csite("client", e), + http_arg_string(e, k, a). +// a default: `defaults: new { controller = "Reports" }`. The parser writes an anonymous +// object's members as its children in order, each name followed by its value. +// (demanded by the callee's name, not by conv_pattern: the pattern's value is a string +// read through str_value, and so are these) +conv_call(e) :- conv_route_call(e, _). +conv_call(e) :- call_callee_name("client", e, n), cs_http_default_route(n, _), csite("client", e). +conv_call(e) :- call_callee_name("client", e, n), cs_http_area_route(n, _, _), csite("client", e). +conv_default_lit(e, key, raw) :- conv_call(e), http_arg(e, _, a), expr_kind("client", a, "ANONYMOUS_OBJECT"), + expr_child("client", a, _, kp, k), expr_written_name("client", k, key), cs_http_route_value_key(key), + expr_child("client", a, _, vp, val), to_number(vp) = to_number(kp) + 1, expr_literal("client", val, _, raw). +str_demand(raw) :- conv_default_lit(_, _, raw). +conv_default(e, key, v) :- conv_default_lit(e, key, raw), str_value(raw, v). + +// where `{name` opens a token in a pattern, and the `}` that closes it +conv_tok_at(p, nm, i) :- conv_pattern(_, p), cs_http_route_value_key(nm), tok = cat("{", nm), + contains(tok, p), i = range(0, strlen(p) - strlen(tok)), substr(p, i, strlen(tok)) = tok, + cs_route_token_end(substr(p, i + strlen(tok), 1)). +conv_has(p, nm) :- conv_tok_at(p, nm, _). +conv_tok_span(p, nm, i, j) :- conv_tok_at(p, nm, i), + j = min k : { k = range(i, strlen(p)), substr(p, k, 1) = "}" }. + +mvc_type_attr_routed(t) :- ctl_has_base(t). +mvc_type_attr_routed(t) :- attr_on(t, "TYPE", n, _), cs_http_api_controller_attr(n). +mvc_method_attr_routed(m) :- method_has_route_tmpl(m). +mvc_method_attr_routed(m) :- method_verb_attr(m, _, a), attr_has_template(a). +mvc_area(t, a) :- attr_on(t, "TYPE", n, at), cs_http_area_attr(n), attr_template(at, a). +mvc_has_area(t) :- mvc_area(t, _). +conv_action(t, m, c, mn) :- mvc_action(t, m), mvc_ctl(t, c), !mvc_type_attr_routed(t), !mvc_method_attr_routed(m), + method_name("client", m, mn). +conv_ctl_ok(e, p, t) :- conv_pattern(e, p), conv_has(p, "controller"), conv_action(t, _, _, _). +conv_ctl_ok(e, p, t) :- conv_pattern(e, p), !conv_has(p, "controller"), conv_default(e, "controller", c), + conv_action(t, _, c, _). +conv_act_ok(e, p, m) :- conv_pattern(e, p), conv_has(p, "action"), conv_action(_, m, _, _). +conv_act_ok(e, p, m) :- conv_pattern(e, p), !conv_has(p, "action"), conv_default(e, "action", a), + conv_action(_, m, _, a). +conv_area_ok(e, p, t, "") :- conv_pattern(e, p), !conv_has(p, "area"), !conv_area_fixed(e, _), + conv_action(t, _, _, _), !mvc_has_area(t). +conv_area_ok(e, p, t, a) :- conv_pattern(e, p), conv_has(p, "area"), !conv_area_fixed(e, _), mvc_area(t, a). +conv_area_ok(e, p, t, a) :- conv_pattern(e, p), conv_area_fixed(e, a), mvc_area(t, a). +conv_use(p, m, c, mn, a) :- conv_action(t, m, c, mn), conv_ctl_ok(e, p, t), conv_act_ok(e, p, m), + conv_area_ok(e, p, t, a). + +// fill the tokens in turn: controller, then action, then area +conv_s1(p, c, s) :- conv_use(p, _, c, _, _), conv_tok_span(p, "controller", i, j), + s = cat(substr(p, 0, i), cat(c, substr(p, j + 1, strlen(p) - j - 1))). +conv_s1(p, c, p) :- conv_use(p, _, c, _, _), !conv_has(p, "controller"). +conv_s2_demand(s, mn) :- conv_use(p, _, c, mn, _), conv_s1(p, c, s). +conv_s2_at(s, i) :- conv_s2_demand(s, _), tok = "{action", contains(tok, s), + i = range(0, strlen(s) - strlen(tok)), substr(s, i, strlen(tok)) = tok, cs_route_token_end(substr(s, i + 7, 1)). +conv_s2_has(s) :- conv_s2_at(s, _). +conv_s2(s, mn, o) :- conv_s2_demand(s, mn), conv_s2_at(s, i), j = min k : { k = range(i, strlen(s)), substr(s, k, 1) = "}" }, + o = cat(substr(s, 0, i), cat(mn, substr(s, j + 1, strlen(s) - j - 1))). +conv_s2(s, mn, s) :- conv_s2_demand(s, mn), !conv_s2_has(s). +conv_s3_demand(o, a) :- conv_use(p, _, c, mn, a), conv_s1(p, c, s), conv_s2(s, mn, o). +conv_s3_at(s, i) :- conv_s3_demand(s, _), tok = "{area", contains(tok, s), + i = range(0, strlen(s) - strlen(tok)), substr(s, i, strlen(tok)) = tok, cs_route_token_end(substr(s, i + 5, 1)). +conv_s3_has(s) :- conv_s3_at(s, _). +conv_s3(s, a, o) :- conv_s3_demand(s, a), conv_s3_at(s, i), j = min k : { k = range(i, strlen(s)), substr(s, k, 1) = "}" }, + o = cat(substr(s, 0, i), cat(a, substr(s, j + 1, strlen(s) - j - 1))). +conv_s3(s, a, s) :- conv_s3_demand(s, a), !conv_s3_has(s). +conv_route(m, r) :- conv_use(p, m, c, mn, a), conv_s1(p, c, s1), conv_s2(s1, mn, s2), conv_s3(s2, a, r). +conv_verb(m, verb) :- conv_route(m, _), method_verb_attr(m, verb, _). +conv_verb(m, verb) :- conv_route(m, _), !method_has_verb(m), cs_http_verb_any(verb). +http_route_raw(m, r, verb) :- conv_route(m, r), conv_verb(m, verb). + // ── the server: minimal APIs ──────────────────────────────────────────────── -map_call(e, verb) :- call_callee_name("client", e, n), cs_http_map_verb(n, verb), csite("client", e). +map_verb_call(e, verb) :- call_callee_name("client", e, n), cs_http_map_verb(n, verb), csite("client", e). +map_methods_call(e) :- call_callee_name("client", e, n), cs_http_map_methods(n), csite("client", e). map_group_call(e) :- call_callee_name("client", e, n), cs_http_map_group(n), csite("client", e). +// MapMethods: the verbs a collection spells, `new[] { "HEAD", "OPTIONS" }`, `["PUT"]`, +// `new List { ... }` or `new[] { HttpMethods.Head }`, inline or in a local. +// A list none of whose elements reads as a verb serves any verb, like a bare [Route]. +map_verbs_expr(e, l) :- map_methods_call(e), http_arg(e, 1, l). +map_verbs_expr(e, i) :- map_methods_call(e), http_arg(e, 1, l), + ref_denotes("client", l, "LOCAL_VARIABLE", v), var_value_initializer("client", v, i). +map_verb_elem(e, x) :- map_verbs_expr(e, l), expr_child("client", l, _, _, init), + expr_kind("client", init, "INITIALIZER"), expr_child("client", init, _, _, x). +map_verb_elem(e, x) :- map_verbs_expr(e, l), expr_child("client", l, "COLLECTION_ELEMENT", _, x). +str_demand(raw) :- map_verb_elem(_, x), expr_literal("client", x, _, raw). +map_methods_verb(e, verb) :- map_verb_elem(e, x), expr_literal("client", x, _, raw), str_value(raw, verb), + cs_http_method_member(_, verb). +map_methods_verb(e, verb) :- map_verb_elem(e, x), expr_kind("client", x, "MEMBER_ACCESS"), + expr_member_name_child("client", x, nm), expr_written_name("client", nm, mn), cs_http_method_member(mn, verb). +map_methods_has_verb(e) :- map_methods_verb(e, _). + +map_call(e, verb) :- map_verb_call(e, verb). +map_call(e, verb) :- map_methods_verb(e, verb). +map_call(e, verb) :- map_methods_call(e), !map_methods_has_verb(e), cs_http_verb_any(verb). + +// the handler is argument 1, or 2 on MapMethods; written there, or held in a local +map_handler_at(e, 1) :- map_verb_call(e, _). +map_handler_at(e, 2) :- map_methods_call(e). +map_handler_expr(e, h) :- map_handler_at(e, k), http_arg(e, k, h). +map_handler_expr(e, i) :- map_handler_at(e, k), http_arg(e, k, h), + ref_denotes("client", h, "LOCAL_VARIABLE", v), var_value_initializer("client", v, i). + // the handler: a method group, `Type.Method`, or a lambda (its own method row) -map_handler(e, m) :- map_call(e, _), http_arg(e, 1, h), expr_anon_decl("client", h, m). -map_handler(e, m) :- map_call(e, _), http_arg(e, 1, h), +map_handler(e, m) :- map_handler_expr(e, h), expr_anon_decl("client", h, m). +map_handler(e, m) :- map_handler_expr(e, h), expr_kind("client", h, "NAME_REFERENCE"), expr_ref_unknown("client", h), expr_written_name("client", h, n), !local_binds("client", h, _), expr_ultimate_type_group("client", h, gk), member_lookup("client", gk, n, "method", m). -map_handler(e, m) :- map_call(e, _), http_arg(e, 1, h), +map_handler(e, m) :- map_handler_expr(e, h), expr_kind("client", h, "MEMBER_ACCESS"), expr_qualifier_child("client", h, q), expr_member_name_child("client", h, nm), expr_written_name("client", nm, n), delegate_qualifier_type("client", q, gk), @@ -253,7 +496,7 @@ gp_demand(init) :- gp_demand(r), ref_denotes("client", r, "LOCAL_VARIABLE", v), gp_demand(rr) :- gp_demand(r), csite("client", r), call_receiver_expr("client", r, rr). gp_is_local(r) :- gp_demand(r), ref_denotes("client", r, "LOCAL_VARIABLE", v), var_initializer("client", v, _). gp_is_call(r) :- gp_demand(r), csite("client", r), call_receiver_expr("client", r, _). -group_prefix(r, "") :- gp_demand(r), !gp_is_local(r), !gp_is_call(r). +group_prefix(r, "") :- gp_demand(r), !gp_is_local(r), !gp_is_call(r), !gp_is_param(r). group_prefix(r, p) :- gp_demand(r), ref_denotes("client", r, "LOCAL_VARIABLE", v), var_initializer("client", v, init), group_prefix(init, p). group_prefix(r, p) :- gp_demand(r), csite("client", r), !map_group_call(r), @@ -261,9 +504,46 @@ group_prefix(r, p) :- gp_demand(r), csite("client", r), !map_group_call(r), group_prefix(r, g) :- gp_demand(r), map_group_call(r), http_arg_string(r, 0, g), call_receiver_expr("client", r, rr), group_prefix(rr, ""). group_prefix(r, p) :- gp_demand(r), map_group_call(r), http_arg_string(r, 0, g), - call_receiver_expr("client", r, rr), group_prefix(rr, pp), pp != "", !starts_slash(g), p = cat(pp, cat("/", g)). + call_receiver_expr("client", r, rr), group_prefix(rr, pp), pp != "", strlen(pp) < 512, !starts_slash(g), + p = cat(pp, cat("/", g)). group_prefix(r, p) :- gp_demand(r), map_group_call(r), http_arg_string(r, 0, g), - call_receiver_expr("client", r, rr), group_prefix(rr, pp), pp != "", starts_slash(g), p = cat(pp, g). + call_receiver_expr("client", r, rr), group_prefix(rr, pp), pp != "", strlen(pp) < 512, starts_slash(g), + p = cat(pp, g). +// (the length bound only stops a helper that maps a sub-group of itself from recursing) + +// A group handed to a helper: `app.MapGroup("/api/orders").MapOrderEndpoints()` with +// `MapOrderEndpoints(this RouteGroupBuilder g) { g.MapGet(...); }`. The parameter takes +// the prefix of the builder at EACH call site, so a helper mapped under two groups +// serves under both. A call binds the helper when it resolved to it, or, since the +// builder type is never staged and such a call resolves to nothing, when it names the +// one route-builder extension of that name and arity in scope. Without a bound call +// the parameter is a root, as before. +gp_bind_param(w, p, k) :- param_decl("client", w, pos, _, p), param_type_name("client", p, tn, _), + cs_http_route_builder_type(tn), k = to_number(pos). +gp_w_nparams(w, n) :- gp_bind_param(w, _, _), n = count : { param_decl("client", w, _, _, _) }. +gp_ext_named(w, n, mod) :- gp_bind_param(w, p, 0), extension_method("client", w, p), + method_name("client", w, n), extension_in_scope("client", mod, w). +gp_named_call(c, n, mod) :- call_callee_name("client", c, n), gp_ext_named(_, n, mod), csite("client", c), + call_module("client", c, mod), call_receiver_expr("client", c, _), !expr_resolves_to_method("client", c, _). +gp_c_nargs(c, n) :- gp_named_call(c, _, _), n = count : { arg_index(c, _, _) }. +gp_c_nargs(c, n) :- gp_bind_param(w, _, _), expr_resolves_to_method("client", c, w), csite("client", c), + n = count : { arg_index(c, _, _) }. +gp_named_cand(c, w) :- gp_named_call(c, n, mod), gp_ext_named(w, n, mod), gp_c_nargs(c, na), gp_w_nparams(w, na + 1). +gp_named_ncand(c, k) :- gp_named_cand(c, _), k = count : { gp_named_cand(c, _) }. +gp_helper_call(c, w) :- gp_bind_param(w, _, _), expr_resolves_to_method("client", c, w), csite("client", c). +gp_helper_call(c, w) :- gp_named_cand(c, w), gp_named_ncand(c, 1). +// extension form: the receiver is parameter 0 and argument k-1 is parameter k +gp_bind_arg(p, a) :- gp_helper_call(c, w), extension_method("client", w, _), gp_c_nargs(c, na), + gp_w_nparams(w, na + 1), gp_bind_param(w, p, 0), call_receiver_expr("client", c, a). +gp_bind_arg(p, a) :- gp_helper_call(c, w), extension_method("client", w, _), gp_c_nargs(c, na), + gp_w_nparams(w, na + 1), gp_bind_param(w, p, k), k > 0, arg_index(c, k - 1, a). +// a plain or statically written call: argument k is parameter k +gp_bind_arg(p, a) :- gp_helper_call(c, w), gp_c_nargs(c, na), gp_w_nparams(w, na), + gp_bind_param(w, p, k), arg_index(c, k, a). +gp_param_arg(r, a) :- gp_demand(r), ref_denotes("client", r, "PARAMETER", p), gp_bind_arg(p, a). +gp_demand(a) :- gp_param_arg(_, a). +gp_is_param(r) :- gp_param_arg(r, _). +group_prefix(r, p) :- gp_param_arg(r, a), group_prefix(a, p). http_route_raw(m, p, verb) :- map_call(e, verb), map_handler(e, m), http_arg_string(e, 0, t), call_receiver_expr("client", e, r), group_prefix(r, ""), p = t. diff --git a/graph/csharp/engine/framework-behavior/dispatch.dl b/graph/csharp/engine/framework-behavior/dispatch.dl index 8b8e07a1..c5311ef1 100644 --- a/graph/csharp/engine/framework-behavior/dispatch.dl +++ b/graph/csharp/engine/framework-behavior/dispatch.dl @@ -155,3 +155,32 @@ med_dispatch(e, m) :- med_declared_handler(e, m). med_send_matcher(e) :- med_send_arg(e, a), call_callee_name("client", a, n0), last_segment(n0, n), cs_mock_arg_matcher(n). call_chain_edge(e, from, "-", m, "client", "event_dispatch", kind) :- med_send_site(e, from, _, _), med_dispatch(e, m), !med_send_matcher(e), from != m, invocation_site("client", e, kind). + +// ── A CLASS MOCK RUNS ITS TYPE'S CONSTRUCTOR ──────────────────────────────── +// `new Mock(2m)` and `Substitute.ForPartsOf(2m)` construct a proxy +// subclass of PriceRule, and the proxy's constructor calls PriceRule(decimal) with the +// arguments given. The site's own target is the library's (Mock's constructor, +// ForPartsOf), so `impact` on the constructor left those tests out and `path` called the +// two independent (#1495). The hop is ADDED beside the site's row, on the tier a +// framework-run hop takes (event_dispatch), as the mediator's is above; the site's own +// resolution is unchanged. The overload is chosen by argument count, as for `new C(args)`. +// Knobs: cs_mock_new_type, cs_mock_factory, cs_mock_behavior_arg. +mock_ctor_site(e, gk) :- expr_new_type_ref("client", e, tr), type_ref_name("client", tr, n0), last_segment(n0, n), + cs_mock_new_type(n), type_ref_arg("client", tr, "0", ar), type_ref_resolves("client", ar, gk). +mock_ctor_site(e, gk) :- call_receiver_text("client", e, rn0), last_segment(rn0, rn), call_callee_name("client", e, sn), + cs_mock_factory(rn, sn), call_type_argc("client", e, "1"), + type_ref_owner("client", tr, e, "EXPRESSION"), type_ref_context("client", tr, "METHOD_TYPE_ARGUMENT"), + !type_ref_parent("client", tr, _, _, _), type_ref_resolves("client", tr, gk). +last_segment_demand(n) :- expr_new_type_ref("client", _, tr), type_ref_name("client", tr, n). +last_segment_demand(n) :- call_receiver_text("client", e, n), call_callee_name("client", e, sn), cs_mock_factory(_, sn). +// `new Mock(() => new C(1))` hands Moq the construction to run: that lambda's own +// `new` is the call, and the site passes no constructor arguments. +mock_ctor_lambda(e) :- mock_ctor_site(e, _), arg_index(e, 0, a), expr_kind("client", a, "LAMBDA"). +mock_ctor_behavior(e) :- mock_ctor_site(e, _), arg_index(e, 0, a), expr_written_name("client", a, w), + cs_mock_behavior_arg(b), strlen(w) > strlen(b), substr(w, 0, strlen(b) + 1) = cat(b, "."). +mock_ctor_argc(e, ac) :- mock_ctor_site(e, _), !mock_ctor_lambda(e), !mock_ctor_behavior(e), call_argc("client", e, ac). +mock_ctor_argc(e, ac) :- mock_ctor_site(e, _), !mock_ctor_lambda(e), mock_ctor_behavior(e), call_argc("client", e, ac0), + ac = to_string(to_number(ac0) - 1). +mock_ctor(e, ctor) :- mock_ctor_site(e, gk), mock_ctor_argc(e, ac), type_ctor("client", gk, ctor), ctor_accepts_argc("client", ctor, ac). +call_chain_edge(e, from, "-", ctor, "client", "event_dispatch", kind) :- mock_ctor(e, ctor), call_from("client", e, from), + from != ctor, invocation_site("client", e, kind). diff --git a/graph/csharp/engine/framework-behavior/entry-points.dl b/graph/csharp/engine/framework-behavior/entry-points.dl index 36d582dd..759405a3 100644 --- a/graph/csharp/engine/framework-behavior/entry-points.dl +++ b/graph/csharp/engine/framework-behavior/entry-points.dl @@ -11,8 +11,13 @@ // about which method is the handler. // ============================================================================ -// main: a static Main, and the method top-level statements compile to -entry_point(m, "main") :- method_name("client", m, "Main"), method_modifier("client", m, "static"). +// main: a static Main of a shape C# accepts as an entry point (void, int, Task or Task, +// with no parameter or one string[]), and the method top-level statements compile to +entry_point(m, "main") :- method_name("client", m, "Main"), method_modifier("client", m, "static"), cs_main_shape(m). +cs_main_shape(m) :- method_name("client", m, "Main"), method_return_type_name("client", m, rt), cs_main_return(rt), + method_paramc("client", m, "0"). +cs_main_shape(m) :- method_name("client", m, "Main"), method_return_type_name("client", m, rt), cs_main_return(rt), + method_paramc("client", m, "1"), param_decl("client", m, "0", _, p), param_type_name("client", p, _, pt), cs_main_param(pt). entry_point(m, "main") :- method_is_top_level_entry("client", m). // test: a test method, and the set-up and tear-down around it @@ -20,6 +25,15 @@ entry_point(m, "test") :- attr_on(m, "METHOD", n, _), cs_test_attr(n). entry_point(m, "test") :- attr_on(m, "METHOD", n, _), cs_test_lifecycle_attr(n). entry_point(m, "test") :- attr_on(m, "METHOD", n0, _), cs_attr_suffix(n0, n), cs_test_attr(n). entry_point(m, "test") :- attr_on(m, "METHOD", n0, _), cs_attr_suffix(n0, n), cs_test_lifecycle_attr(n). +// an attribute class DERIVED from a test attribute is one: xUnit reads `[SlowFact]` through +// `SlowFactAttribute : FactAttribute` as a fact (#1497), transitively. Matched by the base's written name, since +// the framework is not in the source; an attribute derived from anything else (`AuditedAttribute : Attribute`) +// stays out. +cs_test_attr_type(cat(n, "Attribute")) :- cs_test_attr(n). +cs_test_attr_type(n0) :- type_decl("client", n0, _, _, _, t), heritage_slot("client", t, _, bn0, _, _), + last_segment(bn0, bn), cs_test_attr_type(bn). +entry_point(m, "test") :- attr_on(m, "METHOD", n0, _), cs_test_attr_type(n0). +entry_point(m, "test") :- attr_on(m, "METHOD", n0, _), cs_test_attr_type(cat(n0, "Attribute")). // `[FactAttribute]` is the same attribute as `[Fact]` cs_attr_suffix(n0, n) :- attr_on(_, "METHOD", n0, _), strlen(n0) > 9, substr(n0, strlen(n0) - 9, 9) = "Attribute", n = substr(n0, 0, strlen(n0) - 9). diff --git a/graph/csharp/engine/framework-behavior/knobs.dl b/graph/csharp/engine/framework-behavior/knobs.dl index 1ead8281..1a27058d 100644 --- a/graph/csharp/engine/framework-behavior/knobs.dl +++ b/graph/csharp/engine/framework-behavior/knobs.dl @@ -26,8 +26,23 @@ cs_grpc_client_member_not_rpc("NewInstance"). cs_framework_namespace_root("System"). cs_framework_namespace_root("Microsoft"). +// The gRPC runtime's own namespaces. A file `using` them says nothing about which +// namespace a generated service is in, so they never tell two same-named services apart. +cs_grpc_runtime_namespace("Grpc.Core"). +cs_grpc_runtime_namespace("Grpc.Net.Client"). +cs_grpc_runtime_namespace("Grpc.Net.ClientFactory"). +cs_grpc_runtime_namespace("Grpc.AspNetCore.Server"). +cs_grpc_runtime_namespace("Google.Protobuf"). +cs_grpc_runtime_namespace("Google.Protobuf.WellKnownTypes"). +// A factory whose type argument is the client it returns: +// `factory.CreateClient("orders")` (Grpc.Net.ClientFactory). +cs_client_factory_generic("CreateClient"). + cs_remote_transport_grpc("grpc"). cs_remote_confidence_exact("exact"). +// a gRPC send whose service name several services share, none in a namespace the +// client can see: every one is kept, a rung below exact +cs_remote_confidence_service_name("service_name"). // ── HTTP: ASP.NET Core on the server ──────────────────────────────────────── // Attribute routing. `Route` carries a template and no verb; each Http carries a @@ -44,12 +59,38 @@ cs_http_verb_attr("HttpOptions", "OPTIONS"). cs_http_verb_attr("HttpOptionsAttri // The suffix ASP.NET strips from a controller's class name for the [controller] token. cs_http_controller_suffix("Controller"). // Minimal APIs: `app.MapGet(template, handler)`, and a route group's prefix. +// Controllers: the bases that make a class a controller whatever its name, the attribute +// that keeps a public method from being an action, an [Area], and [ApiController], which +// requires attribute routing (conventional routes never reach it). +cs_http_controller_base("Controller"). +cs_http_controller_base("ControllerBase"). +cs_http_non_action_attr("NonAction"). cs_http_non_action_attr("NonActionAttribute"). +cs_http_area_attr("Area"). cs_http_area_attr("AreaAttribute"). +cs_http_api_controller_attr("ApiController"). cs_http_api_controller_attr("ApiControllerAttribute"). +// Conventional routes: the method and the argument its pattern is in. The default route +// has its pattern fixed; an area route names the area in one argument, the pattern in another. +cs_http_conventional_route("MapControllerRoute", 1). +cs_http_conventional_route("MapRoute", 1). +cs_http_area_route("MapAreaControllerRoute", 1, 2). +cs_http_area_route("MapAreaRoute", 1, 2). +cs_http_default_route("MapDefaultControllerRoute", "{controller=Home}/{action=Index}/{id?}"). +// the route values a pattern or its defaults name, and what may follow a token's name +cs_http_route_value_key("controller"). cs_http_route_value_key("action"). cs_http_route_value_key("area"). +cs_route_token_end("}"). cs_route_token_end("="). cs_route_token_end(":"). cs_route_token_end("?"). cs_http_map_verb("MapGet", "GET"). cs_http_map_verb("MapPost", "POST"). cs_http_map_verb("MapPut", "PUT"). cs_http_map_verb("MapDelete", "DELETE"). cs_http_map_verb("MapPatch", "PATCH"). cs_http_map_group("MapGroup"). +// `app.MapMethods(template, verbs, handler)`: the verbs are a collection in argument 1, +// each a literal or a `HttpMethods.Head` member (cs_http_method_member below), and the +// handler is argument 2. +cs_http_map_methods("MapMethods"). +// The builder types an endpoint-mapping helper takes, `this RouteGroupBuilder g`: the +// prefix of the group each call site passes carries into the helper's MapX calls. +cs_http_route_builder_type("RouteGroupBuilder"). +cs_http_route_builder_type("IEndpointRouteBuilder"). // ── HTTP: the client ──────────────────────────────────────────────────────── // A receiver whose written type is HttpClient, and the verb each of its calls sends. @@ -234,6 +275,26 @@ cs_test_lifecycle_attr("OneTimeSetUp"). cs_test_lifecycle_attr("OneTimeTearD cs_test_lifecycle_attr("TestInitialize"). cs_test_lifecycle_attr("TestCleanup"). cs_test_lifecycle_attr("ClassInitialize"). cs_test_lifecycle_attr("ClassCleanup"). cs_test_lifecycle_attr("AssemblyInitialize"). cs_test_lifecycle_attr("AssemblyCleanup"). +// MSTest 3.10+: run around EVERY test of the assembly (#1503) +cs_test_lifecycle_attr("GlobalTestInitialize"). cs_test_lifecycle_attr("GlobalTestCleanup"). + +// cs_main_return(Written) / cs_main_param(Written): the shapes C# accepts as a program's +// entry point. A static `Main` of any other shape, `static string Main()` on a helper, is +// an ordinary method its callers reach, and was a main entry point (#1451). +cs_main_return("void"). cs_main_return("int"). cs_main_return("Int32"). cs_main_return("System.Int32"). +cs_main_return("Task"). cs_main_return("Task"). cs_main_return("Task"). +cs_main_return("System.Threading.Tasks.Task"). cs_main_return("System.Threading.Tasks.Task"). +cs_main_param("string[]"). cs_main_param("String[]"). cs_main_param("System.String[]"). + +// cs_mock_new_type(Type) / cs_mock_factory(Receiver, Method): building a CLASS mock runs the +// mocked type's constructor with the arguments given, through the proxy subclass the +// library generates: `new Mock(args)` (Moq) and `Substitute.ForPartsOf(args)`, +// `Substitute.For(args)` (NSubstitute). cs_mock_behavior_arg is a leading argument +// that configures the mock and is not passed on (Moq's `MockBehavior.Strict`) (#1495). +cs_mock_new_type("Mock"). +cs_mock_factory("Substitute", "ForPartsOf"). +cs_mock_factory("Substitute", "For"). +cs_mock_behavior_arg("MockBehavior"). // cs_framework_callback(Base, Method, Reason): a framework calls Method on a type that // derives from Base or implements it, directly or through the project's own bases. // Base is the written simple name without type arguments. Method "*" is every public 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/delegate-params.dl b/graph/csharp/engine/resolution/delegate-params.dl new file mode 100644 index 00000000..c1b5a58a --- /dev/null +++ b/graph/csharp/engine/resolution/delegate-params.dl @@ -0,0 +1,113 @@ +// ============================================================================ +// RESOLUTION · A DELEGATE HANDED TO A PARAMETER +// +// delegates.dl follows a function stored in a field or property and stops at the +// two shapes it names as NOT HANDLED: a delegate that reaches the member FROM A +// PARAMETER (`_fn = fn`, `this.a = a`, a primary constructor's `fn` read in a field +// initializer), and a parameter of delegate type INVOKED DIRECTLY (`act(1)`, +// `handler.Invoke(call)`, `validatorProvider(x)` inside a lambda that captured it). +// Both are the ordinary way C# hands behaviour to a library -- a strategy object's +// constructor, a builder's `Add(v => ...)`, a callback argument -- and in both the +// function that runs is a lambda or method group written at the CALL SITE, which no +// member lookup can see. +// +// THE SAME ENVELOPE delegates.dl uses, keyed on the parameter rather than the member: +// +// delegate_param_holds(P, F) F may be the function parameter P holds: a lambda, +// an anonymous method or a method group passed as the +// argument at P's ordinal to a call the engine +// resolved to P's method (a constructor too); or a +// parameter of the caller passed on, or a member that +// holds functions passed on. +// delegate_member_holds(M, F) gains: F reaches member M through a parameter that +// is stored in it. +// a call through P an edge to ONE node per parameter, +// generated:#..Invoke +// and method_dispatch_candidate(node, F, "value") for +// each F it holds -- what impact and path already walk +// for a member (F -> node -> whoever calls through P). +// +// Value flow is not context-sensitive here, exactly as for a member: every function +// passed to P at any call is a candidate at every call through P. The resolved site +// target is untouched (Roslyn binds `act(1)` to Invoke), so the per-site score cannot +// move. +// ============================================================================ + +// ── THE ARGUMENT AT AN ORDINAL ────────────────────────────────────────────── +// Among a call's ARGUMENT children, with no named argument at the site (a named one +// may bind anywhere). A call the engine resolved to a method, or a `new`. +delegate_arg_at(e, k, a) :- + expr_argument("client", e, pos, a), !call_has_named_args("client", e), + p = to_number(pos), + k = count : { expr_argument("client", e, q, _), to_number(q) < p }. +delegate_site_callee(e, m, 0) :- + expr_resolves_to_method("client", e, m), !method_is_extension("client", m). +// An extension method called on a receiver takes the receiver as parameter 0. +delegate_site_callee(e, m, 1) :- + expr_resolves_to_method("client", e, m), method_is_extension("client", m), + call_receiver_expr("client", e, _). +delegate_site_callee(e, m, 0) :- + expr_resolves_to_method("client", e, m), method_is_extension("client", m), + !call_receiver_expr("client", e, _). +delegate_site_callee(e, ctor, 0) :- call_ctor_target("client", e, ctor). + +delegate_param_at(m, k, p) :- + param_decl("client", m, pos, _, p), k = to_number(pos), !param_is_params("client", p). + +// delegate_param_receives(P, Arg): Arg is passed to parameter P, and the branches +// inside it (`?:`, `??`, a cast, parentheses). +delegate_param_receives(p, a) :- + delegate_arg_at(e, k, a), delegate_site_callee(e, m, off), delegate_param_at(m, k + off, p). +delegate_param_receives(p, c) :- delegate_param_receives(p, a), delegate_value_branch("client", a, c). + +// ── A REFERENCE TO A PARAMETER ────────────────────────────────────────────── +delegate_param_ref(e, p) :- ref_denotes("client", e, "PARAMETER", p). +// A primary constructor's parameter read in a FIELD or PROPERTY INITIALIZER: +// `private readonly Func _fn = fn ?? Default;`. local-binding.dl binds a name +// inside a member; an initializer belongs to no method, so the name stays unbound. +delegate_param_ref(e, p) :- + delegate_initializer_name(e, n, t), + type_group("client", t, gk), type_ctor("client", gk, ctor), + method_kind("client", ctor, "PRIMARY_CONSTRUCTOR"), + param_decl("client", ctor, _, n, p). +delegate_initializer_name(e, n, t) :- + delegate_store("client", f, e), field_decl("client", _, t, f), + expr_kind("client", e, "NAME_REFERENCE"), expr_ref_unknown("client", e), expr_written_name("client", e, n). +delegate_initializer_name(e, n, t) :- + delegate_store("client", pr, e), property_decl("client", _, t, pr), + expr_kind("client", e, "NAME_REFERENCE"), expr_ref_unknown("client", e), expr_written_name("client", e, n). + +// ── WHAT A PARAMETER HOLDS ────────────────────────────────────────────────── +delegate_param_holds(p, f) :- + delegate_param_receives(p, a), expr_anon_decl("client", a, f), method_decl("client", _, _, _, f). +delegate_param_holds(p, f) :- + delegate_param_receives(p, a), delegate_group_method("client", a, f), method_decl("client", _, _, _, f). +delegate_param_holds(p, f) :- + delegate_param_receives(p, a), delegate_param_ref(a, q), q != p, delegate_param_holds(q, f). +delegate_param_holds(p, f) :- + delegate_param_receives(p, a), delegate_member_ref("client", a, mem), delegate_member_holds("client", mem, f). +delegate_param_holds_any(p) :- delegate_param_holds(p, _). + +// A parameter STORED in a member: `_a = a`, `_fn = fn ?? Zero`. +delegate_member_holds("client", mem, f) :- + delegate_store("client", mem, e), delegate_param_ref(e, p), delegate_param_holds(p, f). + +// ── A CALL THROUGH THE PARAMETER ──────────────────────────────────────────── +// `act(1)`, `handler(x)`, and the explicit `handler.Invoke(x)` / `a?.Invoke()`. +delegate_param_call(e, p) :- + csite("client", e), call_receiver_kind("client", e, "NONE"), + expr_method_name_child("client", e, x), delegate_param_ref(x, p), + delegate_param_holds_any(p). +delegate_param_call(e, p) :- + call_callee_name("client", e, "Invoke"), + call_receiver_expr("client", e, r), delegate_param_ref(r, p), + delegate_param_holds_any(p). + +delegate_param_node(p, id) :- + delegate_param_call(_, p), + param_decl("client", m, _, pn, p), method_decl("client", mn, _, _, m), + method_owner("client", t, m), type_decl("client", _, qn, _, _, t), + id = cat("generated:", cat(qn, cat("#", cat(mn, cat(".", cat(pn, ".Invoke")))))). + +method_dispatch_candidate(base, f, "value") :- + delegate_param_node(p, base), delegate_param_holds(p, f). diff --git a/graph/csharp/engine/resolution/delegates.dl b/graph/csharp/engine/resolution/delegates.dl index 13eb9773..8f6d31ae 100644 --- a/graph/csharp/engine/resolution/delegates.dl +++ b/graph/csharp/engine/resolution/delegates.dl @@ -45,9 +45,24 @@ // right-hand side (`f = Make()`) stores the call's return value, which is not // followed. // +// A FIELD-LIKE EVENT IS FOLLOWED AS A FIELD IS (#1449). `public event EventHandler? +// Ticked;` is a delegate field the compiler wraps in add and remove accessors: `+=` +// adds the handler to the same invocation list, and `Ticked?.Invoke(...)` inside the +// class calls through it. So an event member is one more member kind here, with its +// own Invoke node. An event with explicit accessors cannot be invoked by name (CS0079), +// so treating every event this way adds no call that does not compile; its `+=` keeps +// the event_subscribe accessor edge (properties.dl) beside what is stored. +// +// A METHOD GROUP HANDED OVER (#1550). A method group that is not stored in a member +// this file follows -- an argument (`new Timer(Sweep, ...)`, `Run(Report)`, +// `token.Register(OnStop)`) or the value assigned to a member the engine does not know +// (`AppDomain.CurrentDomain.ProcessExit += OnExit`) -- is handed to code that may +// invoke it, and nothing else names it. method_group_handoff below records it, and +// call_chain.dl emits it as a `callback_registered` edge from the enclosing method. +// // NOT HANDLED, and left exactly as before: a delegate that reaches the field from a -// parameter or a local (`this.f = f`), a local or parameter of delegate type invoked -// directly, and an event (`+=` on an event is an accessor call, accessor-edges). +// parameter or a local (`this.f = f`), and a local or parameter of delegate type +// invoked directly. // ============================================================================ // ── THE MEMBER AN EXPRESSION DENOTES ──────────────────────────────────────── @@ -59,6 +74,8 @@ delegate_member_ref(prov, e, mem) :- expr_kind(prov, e, "NAME_REFERENCE"), ref_denotes(prov, e, "FIELD", mem). delegate_member_ref(prov, e, mem) :- expr_kind(prov, e, "NAME_REFERENCE"), ref_denotes(prov, e, "PROPERTY", mem). +delegate_member_ref(prov, e, mem) :- + expr_kind(prov, e, "NAME_REFERENCE"), ref_denotes(prov, e, "EVENT", mem). delegate_member_ref(prov, e, mem) :- expr_kind(prov, e, "MEMBER_ACCESS"), expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nm), @@ -71,6 +88,7 @@ delegate_qualifier_type(prov, q, gk) :- expr_type(prov, q, gk). delegate_qualifier_type(prov, q, gk) :- ref_names_type(prov, q, gk). delegate_member_kind("field"). delegate_member_kind("property"). +delegate_member_kind("event"). // The null-forgiving `!` and parentheses around a callee or a receiver name the // same member: `named!(url)`, `(f)(url)`. @@ -130,15 +148,18 @@ delegate_value_method(prov, e, m) :- delegate_stored_expr(prov, e), expr_anon_de // the plain positions METHOD_GROUP, but not a name inside `?:` or `??`, so the name // is looked up here rather than read off that mark. Every overload of the name is a // candidate: which one the conversion picks needs the delegate's parameter types. -delegate_value_method(prov, e, m) :- - delegate_stored_expr(prov, e), +delegate_value_method(prov, e, m) :- delegate_group_method(prov, e, m). +delegate_group_demand(prov, e) :- delegate_stored_expr(prov, e). +delegate_group_demand(prov, e) :- delegate_handed_expr(prov, e). +delegate_group_method(prov, e, m) :- + delegate_group_demand(prov, e), expr_kind(prov, e, "NAME_REFERENCE"), expr_ref_unknown(prov, e), expr_written_name(prov, e, n), !local_binds(prov, e, _), expr_ultimate_type_group(prov, e, gk), member_lookup(prov, gk, n, "method", m). // `this.M` and `Type.M`. -delegate_value_method(prov, e, m) :- - delegate_stored_expr(prov, e), +delegate_group_method(prov, e, m) :- + delegate_group_demand(prov, e), expr_kind(prov, e, "MEMBER_ACCESS"), expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nm), expr_written_name(prov, nm, n), @@ -160,6 +181,36 @@ delegate_member_holds(prov, mem, m) :- delegate_member_holds_any(prov, mem) :- delegate_member_holds(prov, mem, _). +// ── A METHOD GROUP HANDED OVER, NOT STORED IN A FOLLOWED MEMBER (#1550) ──── +// An argument, and the branches inside it (`?:`, `??`, a cast, parentheses, +// `new Action(M)`). A lambda argument is left out: it is its own method, already +// defined inside the caller, and an edge to it would say the same thing twice. +delegate_arg_expr(prov, e) :- expr_child(prov, _, "ARGUMENT", _, e). +delegate_arg_expr(prov, c) :- delegate_arg_expr(prov, p), delegate_value_branch(prov, p, c). +// The value of `=`, `+=` or `??=` whose target is not a member this file follows: +// an event or a property of an unstaged type, `AppDomain.CurrentDomain.ProcessExit +// += OnExit`. A target that IS followed stores the method in its envelope instead. +delegate_handed_value(prov, v) :- + expr_kind(prov, a, "ASSIGNMENT"), expr_operator(prov, a, "="), + expr_child(prov, a, "ASSIGNMENT_TARGET", _, t), !delegate_member_ref(prov, t, _), + expr_child(prov, a, "ASSIGNMENT_VALUE", _, v). +delegate_handed_value(prov, v) :- + expr_kind(prov, a, "COMPOUND_ASSIGNMENT"), expr_operator(prov, a, op), delegate_storing_op(op), + expr_child(prov, a, "ASSIGNMENT_TARGET", _, t), !delegate_member_ref(prov, t, _), + expr_child(prov, a, "ASSIGNMENT_VALUE", _, v). +delegate_handed_value(prov, c) :- delegate_handed_value(prov, p), delegate_value_branch(prov, p, c). +delegate_handed_expr(prov, e) :- delegate_arg_expr(prov, e). +delegate_handed_expr(prov, e) :- delegate_handed_value(prov, e). + +// method_group_handoff(Prov, Expr, Method): Expr, in one of those positions, converts +// the method group naming Method. The same lookup a stored value gets, so every +// overload of the name is a candidate: which one the conversion picks needs the +// delegate's parameter types, and each is exported as a hand-off, never as a call. +method_group_handoff(prov, e, m) :- + delegate_handed_expr(prov, e), !expr_anon_decl(prov, e, _), + delegate_group_method(prov, e, m), + method_decl(prov, _, _, _, m). + // ── A CALL THROUGH THE MEMBER ─────────────────────────────────────────────── // delegate_member_call(Prov, Site, Member). // @@ -192,6 +243,8 @@ delegate_member_owner(prov, f, t) :- field_decl(prov, _, t, f). delegate_member_owner(prov, p, t) :- property_decl(prov, _, t, p). delegate_member_name(prov, f, n) :- field_decl(prov, n, _, f). delegate_member_name(prov, p, n) :- property_decl(prov, n, _, p). +delegate_member_owner(prov, ev, t) :- event_decl(prov, _, t, ev). +delegate_member_name(prov, ev, n) :- event_decl(prov, n, _, ev). // delegate_invoke_node(Member, NodeId) -- one per member some call goes through. delegate_invoke_node(mem, id) :- diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl index cfebbc73..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. @@ -246,6 +265,42 @@ extension_match(prov, e, m) :- call_recv_type_name(prov, e, tn), extension_recv_name(prov, m, tn). +// (5) THROUGH THE RECEIVER'S UNSTAGED HERITAGE. An in-source class that derives from +// or implements an unstaged type the `this` parameter names: `class AppServices : +// IServiceCollection` against `this IServiceCollection`. The heritage entry is +// written with the same name the parameter is, so the match is as firm as (3). (No +// test that the `this` type is itself unresolved: that negation would sit inside the +// typing recursion, and a heritage entry that failed to resolve names no staged type.) +extension_match(prov, e, m) :- + call_callee_name(prov, e, n), + expr_ultimate_module(prov, e, mod), + extension_in_scope(prov, mod, m), + method_name(prov, m, n), + call_argc(prov, e, ac), method_accepts_extension_argc(prov, m, ac), + call_recv_type(prov, e, rgk), + type_self_or_ancestor(prov, rgk, agk), + type_base_unresolved(prov, agk, _, tn, _), + extension_recv_name(prov, m, tn). + +// (6) A LIBRARY-TYPED RECEIVER (#1535). With --library, `Kit.Run()` returns the +// staged `Report`, and `Describe(this Report r)` has its `this` type resolved to that +// same library group. The nominal clause (1) walks type_self_or_ancestor, which is +// keyed on the CLIENT's groups and has no row for a library group, not even the +// reflexive one, so the extension matched only without the library and staging it +// made the answer worse. The library's own ancestors are followed the same way. +extension_lib_self_or_ancestor(gk, gk) :- type_group("lib", _, gk). +extension_lib_self_or_ancestor(gk, agk) :- type_ancestor("lib", gk, agk). +extension_match(prov, e, m) :- + call_callee_name(prov, e, n), + expr_ultimate_module(prov, e, mod), + extension_in_scope(prov, mod, m), + method_name(prov, m, n), + call_argc(prov, e, ac), method_accepts_extension_argc(prov, m, ac), + call_recv_type(prov, e, rgk), + type_group("lib", _, rgk), + extension_recv_type(prov, m, tgk), + extension_lib_self_or_ancestor(rgk, tgk). + // THE GATE. An extension method is considered only where instance lookup found // nothing applicable, which is the language's rule and not an optimisation: a type // with its own `Select` must bind to its own, and offering both makes every LINQ @@ -253,6 +308,34 @@ extension_match(prov, e, m) :- extension_candidate(prov, e, m) :- extension_match(prov, e, m), call_instance_lookup_failed(prov, e). +// The candidates of the matching clauses (1)-(6), without rung (7) below: what an +// external label on an unstaged receiver is withheld for (external-types.dl). +extension_candidate_matched(prov, e) :- extension_match(prov, e, _), call_instance_lookup_failed(prov, e). + +// (7) BOTH SIDES UNSTAGED, UNDER DIFFERENT NAMES (#1472): `ServiceCollection services; +// services.AddWidgetRules()` against `AddWidgetRules(this IServiceCollection s)`. The +// receiver's type is unstaged, so the engine has no ancestors for it, and the `this` +// type is unstaged too, so neither nominal nor by-name matching can say whether one +// implements the other. It is the documented DI registration shape, and dropping it +// hid the caller from `impact`. So it is a candidate, but never a firm one: the site +// keeps its external label beside it and reads as one of a set. Taken only where no +// other extension matched the site, so it never widens an answer (3) already gave. +// An in-source `this` type is not this rung: an unstaged type cannot derive from it. +extension_loose_match(prov, e, m) :- + call_callee_name(prov, e, n), + expr_ultimate_module(prov, e, mod), + extension_in_scope(prov, mod, m), + method_name(prov, m, n), + call_argc(prov, e, ac), method_accepts_extension_argc(prov, m, ac), + call_recv_type_name(prov, e, tn), !call_recv_type(prov, e, _), + external_type_name(prov, mod, tn, _), + !extension_recv_type(prov, m, _), + !extension_recv_is_generic(prov, m), !extension_recv_is_object(prov, m), + extension_recv_name(prov, m, xn), xn != tn. +extension_candidate(prov, e, m) :- + extension_loose_match(prov, e, m), + call_instance_lookup_failed(prov, e), + !extension_match(prov, e, _). // A candidate the engine has real evidence for, as opposed to one it reached by // assuming an unanswerable question was answered "nothing". Only a FIRM candidate @@ -297,6 +380,7 @@ extension_is_guess(prov, e, m) :- extension_match(prov, e, m), extension_recv_is_generic(prov, m), !call_recv_type(prov, e, _). +extension_is_guess(prov, e, m) :- extension_loose_match(prov, e, m), !extension_match(prov, e, _). // An extension method is STATIC and is never virtually dispatched: the receiver is // an ordinary argument, so the target is exact and needs no fan. diff --git a/graph/csharp/engine/resolution/external-types.dl b/graph/csharp/engine/resolution/external-types.dl index 837f10ef..966db7a1 100644 --- a/graph/csharp/engine/resolution/external-types.dl +++ b/graph/csharp/engine/resolution/external-types.dl @@ -87,6 +87,25 @@ expr_external_type_name(prov, e, n) :- property_decl(prov, _, t, p), type_module(prov, t, mod), external_type_name(prov, mod, n, _). +// A QUALIFIED property or field read: `result.Errors.Add(x)` where `Errors` is declared +// `List`. The rules above take a bare name the parser bound; a member access +// is bound by member lookup on its qualifier, and its declared type names the receiver +// the same way. Without it the call was left unresolved and read as a call BY NAME to +// every project method called Add. +expr_external_type_name(prov, e, n) :- + property_access(prov, e, p, _), property_type_name(prov, p, n, _), + property_decl(prov, _, t, p), type_module(prov, t, mod), + external_type_name(prov, mod, n, _). +expr_external_type_name(prov, e, n) :- + expr_qualified_field(prov, e, f), field_type_name(prov, f, n, _), + field_decl(prov, _, t, f), type_module(prov, t, mod), + external_type_name(prov, mod, n, _). +expr_qualified_field(prov, e, f) :- + expr_kind(prov, e, "MEMBER_ACCESS"), expr_role(prov, e, "RECEIVER"), + expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nm), + expr_written_name(prov, nm, n), expr_type(prov, q, gk), + member_lookup(prov, gk, n, "field", f). + // A METHOD WHOSE DECLARED RETURN TYPE IS UNSTAGED: `List Items()`. Named by the // ROOT reference (`List`), not by the written text (`List`), so the label is the // same whatever the type argument, as it is for a parameter or a field of that type. @@ -244,6 +263,21 @@ expr_predefined_recv_name(prov, e, n) :- ref_denotes(prov, e, "FIELD", f), field_type_name(prov, f, n, _), predefined_boundary_alias(n). expr_predefined_recv_name(prov, e, n) :- ref_denotes(prov, e, "PROPERTY", p), property_type_name(prov, p, n, _), predefined_boundary_alias(n). +// A KEYWORD type as a static receiver: `string.Format(..)`, `int.Parse(..)`. A keyword +// is never a variable, so the name alone says the call is on the framework type. +expr_predefined_recv_name(prov, e, n) :- + expr_kind(prov, e, "NAME_REFERENCE"), expr_role(prov, e, "RECEIVER"), + expr_written_name(prov, e, n), predefined_boundary_alias(n). +// The parser writes a keyword receiver with no name at all (every identifier it does +// name), so a nameless receiver name is a keyword type; which one is not recorded. +expr_predefined_recv_name(prov, e, "predefined") :- + expr_kind(prov, e, "NAME_REFERENCE"), expr_role(prov, e, "RECEIVER"), + !expr_written_name(prov, e, _), !ref_denotes(prov, e, _, _). +// and a qualified property or field declared with a keyword type: `ctx.Name.Trim()` +expr_predefined_recv_name(prov, e, n) :- + property_access(prov, e, p, _), property_type_name(prov, p, n, _), predefined_boundary_alias(n). +expr_predefined_recv_name(prov, e, n) :- + expr_qualified_field(prov, e, f), field_type_name(prov, f, n, _), predefined_boundary_alias(n). // A LITERAL receiver: `"text".Trim()`. // A RECEIVER THAT IS ITSELF A CALL, returning a predefined type. `sb.ToString()` // on a staged StringBuilder resolves, and its declared return type is `string`, @@ -271,7 +305,9 @@ external_target(prov, e, label) :- external_type_name(prov, mod, tn, _), call_callee_name(prov, e, m), !call_recv_type(prov, e, _), - !extension_candidate(prov, e, _), + // A candidate of the loose rung (7) in extensions.dl keeps the label, so the + // site reads as one of a set rather than committing to the guess. + !extension_candidate_matched(prov, e), label = cat("external:", cat(tn, cat(".", m))). // ── AN OPERATOR ON AN UNSTAGED OPERAND TYPE ───────────────────────────────── @@ -324,12 +360,18 @@ external_target(prov, e, label) :- // the answer is uncertain. So an external target is added ALONGSIDE the local one. // The local edge is not removed -- it may well be right -- and the site becomes // multi_inferred, which is the truthful claim: it is one of these two. +// +// ONLY AN ENTRY LOOKUP CAN REACH (#1431). `class Order : OrderBase, IComparable` +// has an unstaged entry, but it is an interface after the base class, and lookup on +// an Order receiver never considers an interface's members: `external:IComparable.Total` +// beside `Order.Total` named a target that cannot exist. type_member_base_unresolved +// keeps position 0 of a class (a base class or an interface, undecidable by name) and +// every entry of an interface. external_target(prov, e, label) :- call_recv_type(prov, e, gk), - type_hierarchy_incomplete(prov, gk), + type_member_base_unresolved(prov, gk, bn), call_callee_name(prov, e, n), member_method(prov, gk, n, _), - type_base_unresolved(prov, gk, _, bn, _), label = cat("external:", cat(bn, cat(".", n))). // The same for an UNQUALIFIED call, whose receiver is the implicit `this`: the @@ -337,10 +379,27 @@ external_target(prov, e, label) :- external_target(prov, e, label) :- call_is_unqualified(prov, e), expr_ultimate_type_group(prov, e, gk), - type_hierarchy_incomplete(prov, gk), + type_member_base_unresolved(prov, gk, bn), call_callee_name(prov, e, n), member_method(prov, gk, n, _), - type_base_unresolved(prov, gk, _, bn, _), + label = cat("external:", cat(bn, cat(".", n))). + +// ── `base.M()` THROUGH AN UNSTAGED BASE CLASS (#1450) ─────────────────────── +// `base.OnModelCreating(mb)` in a class deriving from an unstaged DbContext. The +// receiver is typed only through a RESOLVED base (callee-resolution.dl), so the site +// had neither a target nor a label, was ambiguous_unknown, and `impact` then offered +// it by name as a caller of every sibling context's OnModelCreating, which it can +// never call: `base.` binds to the base class's member, non-virtually. It is named +// here as the unstaged base's member. Position 0 of a class is the only place a base +// class can be written; a class whose declared base resolved is left to the staged +// lookup, and one with no heritage has nothing to name. +external_target(prov, e, label) :- + call_is_base(prov, e), + expr_ultimate_type_group(prov, e, gk), + !type_has_declared_base(prov, gk), + type_base_unresolved(prov, gk, "0", bn, _), + type_group(prov, t, gk), type_is_class(prov, t), + call_callee_name(prov, e, n), label = cat("external:", cat(bn, cat(".", n))). // ── A MEMBER THE STAGED LIBRARY DOES NOT CARRY IS STILL NAMED ─────────────── @@ -350,11 +409,16 @@ external_target(prov, e, label) :- // where the stub carries the one-argument overload and the source calls the // two-argument one, and `string.Any`, an extension method on no version of the // type. The type IS known, so the site is still named. +// +// NOT WHERE AN IN-SOURCE EXTENSION IS THE ANSWER (#1535). `Kit.Run().Describe()` with +// `Describe(this Report r)` in the project is that extension, and labelling it a +// member of the library's Report hid the call from the method it reaches. external_target(prov, e, label) :- call_recv_type(prov, e, gk), type_group("lib", _, gk), call_callee_name(prov, e, n), !member_method(prov, gk, n, _), + !extension_candidate_firm(prov, e, _), type_decl("lib", bn, _, _, _, t), type_group("lib", t, gk), label = cat("external:", cat(bn, cat(".", n))). @@ -363,6 +427,7 @@ external_target(prov, e, label) :- type_group("lib", _, gk), call_callee_name(prov, e, n), call_arity_no_member(prov, e), + !extension_candidate_firm(prov, e, _), type_decl("lib", bn, _, _, _, t), type_group("lib", t, gk), label = cat("external:", cat(bn, cat(".", n))). diff --git a/graph/csharp/engine/resolution/lambda-parameters.dl b/graph/csharp/engine/resolution/lambda-parameters.dl index ecfc8ce5..f13187a1 100644 --- a/graph/csharp/engine/resolution/lambda-parameters.dl +++ b/graph/csharp/engine/resolution/lambda-parameters.dl @@ -51,6 +51,60 @@ lambda_target_ref(prov, c, tr) :- expr_cast_ref(prov, e, tr), expr_child(prov, e // Through `?:`, `??`, parentheses and `!`. lambda_target_ref(prov, c, tr) :- lambda_target_ref(prov, p, tr), delegate_value_branch(prov, p, c). +// ── A LAMBDA PASSED AS AN ARGUMENT, TO A CALL THAT HAS ONE CANDIDATE ──────── +// `r.Setup(o => o.Reset())` with `Registry Setup(Action configure)`: +// the lambda is converted to the parameter it is passed to, so `o` is a +// WidgetOptions (#1442). The header's caveat is about OVERLOADS, where which +// parameter binds depends on the lambda; so this takes the parameter only where +// the callee has no overload to choose between: no other method of that name in +// its declaring type, and the parameter is not a `params` one. That is decided +// from the declarations alone, so it cannot feed back into the overload choice it +// would otherwise depend on. +// +// The ordinal is the argument's among the ARGUMENT children, with no named +// argument at the site (a named one may bind anywhere). An extension method called +// on a receiver (`services.Configure(o => ...)`) takes its receiver as parameter 0, +// so the argument at ordinal k fills parameter k + 1. +lambda_target_ref(prov, a, tr) :- + lambda_arg_at(prov, e, k, a), + expr_target_any(prov, e, m), lambda_callee_single(prov, m), !method_is_extension(prov, m), + lambda_callee_param_ref(prov, m, k, tr). +lambda_target_ref(prov, a, tr) :- + lambda_arg_at(prov, e, k, a), + expr_target_any(prov, e, m), lambda_callee_single(prov, m), method_is_extension(prov, m), + call_receiver_expr(prov, e, r), expr_type(prov, r, _), + lambda_callee_param_ref(prov, m, k + 1, tr). + +// A lambda ELEMENT of a collection initializer is the argument of the Add the element +// runs (call-edge-generation/implicit-calls.dl): `new Rules { v => v.RuleFor(x => x.Name) }` +// passes the lambda to `Add(Func creator)`, so `v` is that parameter's first +// type argument. The same single-candidate guard as an argument. +lambda_target_ref(prov, el, tr) :- + expr_kind(prov, oc, "OBJECT_CREATION"), expr_type(prov, oc, gk), + expr_child(prov, oc, "INITIALIZER_VALUE", _, i), expr_kind(prov, i, "INITIALIZER"), + expr_parent(prov, el, i, _, _), expr_anon_decl(prov, el, _), + member_method(prov, gk, "Add", m), lambda_callee_single(prov, m), method_paramc(prov, m, "1"), + lambda_callee_param_ref(prov, m, 0, tr). + +// A lambda among a call's ARGUMENT children, by ordinal, at a site with no named argument. +lambda_arg_at(prov, e, k, a) :- + expr_argument(prov, e, pos, a), expr_anon_decl(prov, a, _), + !call_has_named_args(prov, e), + p = to_number(pos), + k = count : { expr_argument(prov, e, q, _), to_number(q) < p }. + +lambda_callee_single(prov, m) :- + method_in_type(prov, gk, m), method_name(prov, m, n), + !lambda_callee_overloaded(prov, gk, n). +lambda_callee_overloaded(prov, gk, n) :- + method_in_type(prov, gk, m1), method_name(prov, m1, n), + method_in_type(prov, gk, m2), method_name(prov, m2, n), m1 != m2. + +// The root type reference of the callee's parameter at an ordinal, not a `params` one. +lambda_callee_param_ref(prov, m, k, tr) :- + param_decl(prov, m, pos, _, p), k = to_number(pos), + !param_is_params(prov, p), param_type_ref(prov, p, tr). + // A property's declared type, as a reference. property_type reads the NAME, which // drops the arguments `Func` is made of. lambda_property_type_ref(prov, p, tr) :- 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/member-lookup.dl b/graph/csharp/engine/resolution/member-lookup.dl index 88f67303..5193e110 100644 --- a/graph/csharp/engine/resolution/member-lookup.dl +++ b/graph/csharp/engine/resolution/member-lookup.dl @@ -43,7 +43,8 @@ member_cand(prov, gk, n, kind, mem, d) :- // The chain may cross into a staged library. member_cand("client", gk, n, kind, mem, d) :- type_base_chain("client", gk, bgk, d), - type_member_name("lib", bgk, n, kind, mem). + type_member_name("lib", bgk, n, kind, mem), + !lib_member_inaccessible(mem). // AND THE RECEIVER'S OWN TYPE MAY BE THE LIBRARY'S. `void M(Greeter g) => g.Hello()` // asks member_lookup("client", , "Hello"), and the members of that group @@ -52,7 +53,8 @@ member_cand("client", gk, n, kind, mem, d) :- // client simply holding one. Without it a call on a library-typed receiver resolves // to nothing even with the library staged and its name resolved. member_cand("client", gk, n, kind, mem, 0) :- - type_member_name("lib", gk, n, kind, mem). + type_member_name("lib", gk, n, kind, mem), + !lib_member_inaccessible(mem). // AND THE LIBRARY TYPE'S OWN BASE CHAIN. The clause above is distance 0 only, so // a client site saw a staged type's own members and none it inherits -- and the @@ -64,7 +66,28 @@ member_cand("client", gk, n, kind, mem, 0) :- // invisible. That is most of `object`: ToString, Equals, GetHashCode, GetType. member_cand("client", gk, n, kind, mem, d) :- type_base_chain("lib", gk, bgk, d), - type_member_name("lib", bgk, n, kind, mem). + type_member_name("lib", bgk, n, kind, mem), + !lib_member_inaccessible(mem). + +// ── A LIBRARY MEMBER THE CLIENT CANNOT ACCESS IS NOT A MEMBER TO IT ───────── +// The three clauses above cross from client code into a staged library, and C# +// member lookup removes every member that is not accessible at the site before it +// picks the nearest one (spec 12.5). A `private`, `internal` or `private protected` +// member of another assembly is never accessible from the client, so it neither +// resolves a client access nor hides anything: inside `Gadget : WidgetBase`, the +// unqualified `Limits` in `Limits.Allow(n)` is the client's own class `Limits`, not +// the library base's `internal List Limits` (#1538). Without it the inherited +// internal member won the simple-name lookup and the call went to a library getter. +// `protected` and `protected internal` stay: a derived client type may use them. +// InternalsVisibleTo is not modelled; an assembly that grants it is rare, and the +// cost there is a missed edge, not a wrong one. +lib_member_inaccessible(m) :- lib_cs_method(_, _, _, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, m), cs_access_outside_assembly_denied(a). +lib_member_inaccessible(p) :- lib_cs_property(_, _, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, p), cs_access_outside_assembly_denied(a). +lib_member_inaccessible(f) :- lib_cs_field(_, _, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, f), cs_access_outside_assembly_denied(a). +lib_member_inaccessible(ev) :- lib_cs_event(_, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, ev), cs_access_outside_assembly_denied(a). +cs_access_outside_assembly_denied("PRIVATE"). +cs_access_outside_assembly_denied("INTERNAL"). +cs_access_outside_assembly_denied("PRIVATE_PROTECTED"). // ── AN EXPLICIT INTERFACE IMPLEMENTATION IS EXCLUDED ──────────────────────── // It is not a member of the declaring type. Removed from the candidate set rather @@ -90,6 +113,36 @@ member_lookup(prov, gk, n, kind, mem) :- member_best_rank(prov, gk, n, d), !member_is_explicit_impl(prov, mem). +// ── AN OVERRIDE DOES NOT HIDE ITS BASE'S OVERLOADS ────────────────────────── +// For a METHOD the nearest declaration is not the whole answer. C# member lookup +// removes an override from the candidate set (it is the virtual member it +// overrides, not a new one) and a derived method hides only a base method of the +// same signature. So a type that overrides one `ValidateAsync` overload still has +// its base's other `ValidateAsync` overloads, and `v.ValidateAsync(person)` on it +// binds to the base's `ValidateAsync(T)`. The nearest-rank rule above stopped at the +// override and the call resolved to it, or to nothing. +// +// Kept to the case where every nearer declaration of the name is an override: a +// derived type that declares a NEW method of the name makes the base's applicable +// only when none of its own are, which needs the argument types; that case keeps +// the nearest answer. A base candidate is still hidden by a nearer one with the +// same signature text, which is the override of that very member. +member_method_newdecl(prov, gk, n, d) :- + member_cand(prov, gk, n, "method", m, d), !member_is_explicit_impl(prov, m), !method_is_override(_, m). +member_method_newdecl_before(prov, gk, n, d) :- + member_cand(prov, gk, n, "method", _, d), member_method_newdecl(prov, gk, n, d2), d2 < d. +member_method_sig_hidden(prov, gk, n, m, d) :- + member_cand(prov, gk, n, "method", m, d), method_signature(_, m, s), + member_cand(prov, gk, n, "method", m2, d2), d2 < d, method_signature(_, m2, s), + !member_is_explicit_impl(prov, m2). +member_lookup(prov, gk, n, "method", mem) :- + member_cand(prov, gk, n, "method", mem, d), + member_best_rank(prov, gk, n, d0), d > d0, + member_cand(prov, gk, n, "method", _, d0), + !member_is_explicit_impl(prov, mem), + !member_method_newdecl_before(prov, gk, n, d), + !member_method_sig_hidden(prov, gk, n, mem, d). + // ── INTERFACE MEMBERS ─────────────────────────────────────────────────────── // A call through an interface-typed receiver looks up in the interface and in every // interface it extends. No distance ordering: interface inheritance is a lattice and diff --git a/graph/csharp/engine/resolution/properties.dl b/graph/csharp/engine/resolution/properties.dl index 2c760ad5..2ff3aa0b 100644 --- a/graph/csharp/engine/resolution/properties.dl +++ b/graph/csharp/engine/resolution/properties.dl @@ -101,6 +101,31 @@ property_access(prov, acc, p, gk) :- // implicit `this`. The parser binds these itself (referencedEntityKind PROPERTY), // so the engine takes its answer rather than re-deriving it. property_access_unqualified(prov, e, p) :- ref_denotes(prov, e, "PROPERTY", p). +// A NULL-CONDITIONAL READ: `ctx?.Parser?.Row`. The parser writes `a?.B` as a member +// access whose MEMBER_NAME child is itself a nameless member access holding the name +// `B` as its qualifier, so the rules above, which read a written member name, never +// saw it, and every property read in a `?.` chain had no edge. The property is looked +// up on the qualifier's type and the site is the name; the access takes its type. +cond_binding(prov, acc, q, nm, n) :- + expr_kind(prov, acc, "MEMBER_ACCESS"), expr_qualifier_child(prov, acc, q), + expr_member_name_child(prov, acc, b), expr_kind(prov, b, "MEMBER_ACCESS"), !expr_written_name(prov, b, _), + expr_qualifier_child(prov, b, nm), expr_kind(prov, nm, "NAME_REFERENCE"), expr_written_name(prov, nm, n). +property_access(prov, nm, p, gk) :- + cond_binding(prov, _, q, nm, n), expr_type(prov, q, gk), member_lookup(prov, gk, n, "property", p). +expr_type(prov, acc, t) :- cond_binding(prov, acc, _, nm, _), expr_type(prov, nm, t). +expr_type(prov, nm, t) :- cond_binding(prov, _, _, nm, _), property_access(prov, nm, p, _), property_type(prov, p, t). + +// AN OBJECT INITIALIZER NAMES A MEMBER OF THE OBJECT IT CREATES. In `new Cfg { Size = 4 }` +// the target `Size` is looked up on Cfg, not in the enclosing method's scope, so neither +// rule above saw it and the set accessor every initializer runs had no edge (a third to +// a half of the property writes a configuration-heavy codebase makes). +property_access(prov, e, p, gk) :- + expr_role(prov, e, "INITIALIZER_TARGET"), expr_kind(prov, e, "NAME_REFERENCE"), + expr_written_name(prov, e, n), + expr_parent(prov, e, asg, _, _), expr_kind(prov, asg, "ASSIGNMENT"), + expr_parent(prov, asg, init, _, _), expr_kind(prov, init, "INITIALIZER"), + expr_parent(prov, init, oc, _, _), expr_type(prov, oc, gk), + member_lookup(prov, gk, n, "property", p). // ── READ OR WRITE ─────────────────────────────────────────────────────────── // The edge role decides: an ASSIGNMENT_TARGET is a write, everything else is a @@ -232,6 +257,34 @@ accessor_edge(prov, e, acc, "indexer") :- indexer_write_target(prov, e, acc). accessor_edge(prov, e, acc, "event_subscribe") :- event_add_target(prov, e, acc). accessor_edge(prov, e, acc, "event_subscribe") :- event_remove_target(prov, e, acc). +// THE DECLARATION A SITE IS WRITTEN AGAINST, when it has no body: an interface or an +// abstract property's accessor. The fan above names the bodies that can run; the +// declaration is in none of them, and without its own edge an interface property has +// no reader at all. The same edge a bodiless method gets in call_chain.dl. +accessor_site_decl(prov, e, decl, "property_read") :- + property_access(prov, e, p, _), !expr_is_write_target(prov, e), property_getter(prov, p, decl). +accessor_site_decl(prov, e, decl, "property_read") :- + property_access_unqualified(prov, e, p), !expr_is_write_target(prov, e), property_getter(prov, p, decl). +accessor_site_decl(prov, e, decl, "property_write") :- + property_access(prov, e, p, _), expr_is_write_target(prov, e), property_setter(prov, p, decl). +accessor_site_decl(prov, e, decl, "property_write") :- + property_access_unqualified(prov, e, p), expr_is_write_target(prov, e), property_setter(prov, p, decl). +accessor_site_decl(prov, e, decl, "property_write") :- + property_access(prov, e, p, _), expr_is_readwrite_target(prov, e), property_setter(prov, p, decl). +accessor_site_decl(prov, e, decl, "indexer") :- + indexer_access(prov, e, p), !expr_is_write_target(prov, e), indexer_getter(prov, p, decl). +accessor_site_decl(prov, e, decl, "indexer") :- + indexer_access(prov, e, p), expr_is_write_target(prov, e), indexer_setter(prov, p, decl). +accessor_site_decl(prov, e, decl, "event_subscribe") :- event_access(prov, e, ev), event_adder(prov, ev, decl). +accessor_site_decl(prov, e, decl, "event_subscribe") :- event_unaccess(prov, e, ev), event_remover(prov, ev, decl). +accessor_is_bodiless(prov, acc) :- + method_is_accessor(prov, acc), method_owner(prov, t, acc), type_is_interface(prov, t), + !interface_has_default_impl(prov, acc). +accessor_is_bodiless(prov, acc) :- method_accessor_of(prov, acc, p, _), property_modifier(prov, p, "abstract"). +accessor_names_bodiless(prov, e, decl, kind) :- + accessor_site_decl(prov, e, decl, kind), accessor_is_bodiless(prov, decl), + !accessor_edge(prov, e, decl, kind), accessor_edge(prov, e, _, kind). + // How many accessors a site can reach. One is the ordinary case; more means the // property is virtual and the receiver's type is open, which is the same fan as a // method call and is labelled the same way. diff --git a/graph/csharp/engine/resolution/type-hierarchy.dl b/graph/csharp/engine/resolution/type-hierarchy.dl index a88dfd53..7c285be1 100644 --- a/graph/csharp/engine/resolution/type-hierarchy.dl +++ b/graph/csharp/engine/resolution/type-hierarchy.dl @@ -201,6 +201,24 @@ type_base_unresolved(prov, gk, pos, bn, ba) :- // instead of ambiguous_unknown. type_hierarchy_incomplete(prov, gk) :- type_base_unresolved(prov, gk, _, _, _). +// type_member_base_unresolved(Prov, GroupKey, BaseName) -- the unresolved entries +// MEMBER LOOKUP on a receiver of this type can reach (#1431). C# looks a member up on +// a class or struct receiver through the class chain only, never through the +// interfaces it implements, and a base class can only be written at position 0 of a +// class. So an unstaged entry after a class's first, and every entry of a struct, is +// an interface whose members the type must declare itself: a member found on the +// type is the answer, and `external:IComparable.Total` beside it names a target no +// lookup can reach. On an interface every entry is a base interface, and lookup does +// go through it. An unstaged entry AT position 0 of a class is either a base class or +// an interface, which the name alone cannot tell (an `I` prefix is a convention, not +// proof), so it keeps the boundary. The looser type_hierarchy_incomplete is kept for +// a MISS, which the boundary still explains when nothing is declared. +type_member_base_unresolved(prov, gk, bn) :- + type_base_unresolved(prov, gk, "0", bn, _), type_group(prov, t, gk), type_is_class(prov, t). +type_member_base_unresolved(prov, gk, bn) :- + type_base_unresolved(prov, gk, _, bn, _), type_group(prov, t, gk), type_is_interface(prov, t). +type_members_incomplete(prov, gk) :- type_member_base_unresolved(prov, gk, _). + // ── ANCESTORS ─────────────────────────────────────────────────────────────── // type_parent -- one relation over both kinds, for reachability. The two are kept // distinct above because dispatch needs them distinct; here they are unioned because 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/engine/resolution/virtual-dispatch.dl b/graph/csharp/engine/resolution/virtual-dispatch.dl index a429165f..d3590f56 100644 --- a/graph/csharp/engine/resolution/virtual-dispatch.dl +++ b/graph/csharp/engine/resolution/virtual-dispatch.dl @@ -94,17 +94,43 @@ implements_shape("client", impl, ifaceM) :- method_paramc("client", impl, pc), method_paramc("lib", ifaceM, pc), method_type_arity("client", impl, ar), method_type_arity("lib", ifaceM, ar). -// The explicit form, matched by the interface NAME written on the declaration. +// The explicit form, matched by the interface NAME written on the declaration. A +// generic interface is written with its arguments -- `IOpt IOpt.Dep()` -- +// and the declaration's name carries none, so the name is compared up to its `<`. +explicit_iface_written(s) :- method_explicit_interface(_, _, s). +explicit_iface_written(s) :- property_explicit_interface(_, _, s). +explicit_iface_base(s, s) :- explicit_iface_written(s), !contains("<", s). +explicit_iface_base(s, b) :- + explicit_iface_written(s), contains("<", s), + i = range(1, strlen(s)), substr(s, i, 1) = "<", b = substr(s, 0, i), !contains("<", b). implements_shape(prov, impl, ifaceM) :- method_explicit_interface(prov, impl, ifaceName), + explicit_iface_base(ifaceName, ifaceBase), method_in_type(prov, gk, impl), type_satisfies(prov, gk, igk), type_group(prov, it, igk), type_decl(prov, iname, iqn, _, _, it), - (ifaceName = iname ; ifaceName = iqn), + (ifaceBase = iname ; ifaceBase = iqn), interface_member(prov, igk, n, "method", ifaceM), method_name(prov, impl, n), method_paramc(prov, impl, pc), method_paramc(prov, ifaceM, pc). +// An explicit PROPERTY, indexer or event: `int IOpt.Count { get; }`. The name is +// written on the property, not on its accessors, so each accessor is paired with the +// interface accessor of the same name (get_Count, set_Count, get_this[], add_E). +implements_shape(prov, impl, ifaceM) :- + accessor_is_explicit(prov, impl, ifaceName), + explicit_iface_base(ifaceName, ifaceBase), + method_in_type(prov, gk, impl), + type_satisfies(prov, gk, igk), + type_group(prov, it, igk), type_decl(prov, iname, iqn, _, _, it), + (ifaceBase = iname ; ifaceBase = iqn), + method_in_type(prov, igk, ifaceM), method_is_accessor(prov, ifaceM), + method_name(prov, impl, n), method_name(prov, ifaceM, n), + method_paramc(prov, impl, pc), method_paramc(prov, ifaceM, pc). +accessor_is_explicit(prov, acc, iface) :- + method_accessor_of(prov, acc, p, _), property_explicit_interface(prov, p, iface). +member_is_explicit_impl(prov, acc) :- accessor_is_explicit(prov, acc, _). + // AND NOT WHERE A PARAMETER TYPE PROVES THEM DIFFERENT (#1246). Name and count alone // paired an explicit `IKeyed.K(string)` with `IKeyed.K(int)`, so the fan of a call the // arguments had narrowed to K(int) still reached K(string). An implementation repeats @@ -252,6 +278,51 @@ dispatch_runtime_shape("client", m, t) :- method_paramc("lib", m, pc), method_paramc("client", t, pc), method_type_arity("lib", m, ar), method_type_arity("client", t, ar). +// ── ACCESSORS DISPATCH LIKE METHODS ───────────────────────────────────────── +// `s.Area` on an abstract `Shape`, `p.Len` on an `IPlain`, `o[i]` on an interface +// indexer: the accessor that runs is the runtime type's own. Accessors are kept out of +// the name table (member_method cannot find them), so the per-runtime-type lookup above +// never answered for one and a read through an interface or a virtual property stopped +// at the declaration, which has no body. Looked up here by the accessor's own name +// (get_Area, set_Len, get_this[]) along the class chain, nearest first, with the +// parameter count separating indexer overloads. +accessor_cand(prov, gk, n, acc, 0) :- + method_in_type(prov, gk, acc), method_is_accessor(prov, acc), method_name(prov, acc, n), + !accessor_is_explicit(prov, acc, _). +accessor_cand(prov, gk, n, acc, d) :- + type_base_chain(prov, gk, bgk, d), + method_in_type(prov, bgk, acc), method_is_accessor(prov, acc), method_name(prov, acc, n), + !accessor_is_explicit(prov, acc, _). +accessor_cand("client", gk, n, acc, d) :- + type_base_chain("client", gk, bgk, d), + method_in_type("lib", bgk, acc), method_is_accessor("lib", acc), method_name("lib", acc, n), + !accessor_is_explicit("lib", acc, _). +accessor_best(prov, gk, n, pc, d) :- + accessor_cand(prov, gk, n, a, _), method_paramc_any(a, pc), + d = min k : { accessor_cand(prov, gk, n, a2, k), method_paramc_any(a2, pc) }. +accessor_member(prov, gk, n, pc, acc) :- + accessor_cand(prov, gk, n, acc, d), method_paramc_any(acc, pc), accessor_best(prov, gk, n, pc, d). +method_paramc_any(m, pc) :- method_paramc(_, m, pc). + +// An accessor is dispatchable when its property is: `virtual`, `abstract` or `override` +// is written on the property, and the accessor rows carry none of it. +method_is_dispatchable(prov, acc) :- + method_accessor_of(prov, acc, p, _), property_modifier(prov, p, mod), + (mod = "virtual" ; mod = "abstract" ; mod = "override"). + +dispatch_runtime_shape(prov, m, t) :- + method_is_accessor(prov, m), method_is_dispatchable(prov, m), + method_in_type(prov, ogk, m), method_name(prov, m, n), method_paramc(prov, m, pc), + type_self_or_ancestor(prov, cgk, ogk), + type_is_concrete(prov, cgk), + accessor_member(prov, cgk, n, pc, t). +dispatch_runtime_shape("client", m, t) :- + method_is_accessor("lib", m), method_is_dispatchable("lib", m), + method_in_type("lib", ogk, m), method_name("lib", m, n), method_paramc("lib", m, pc), + type_self_or_ancestor("client", cgk, ogk), + type_is_concrete("client", cgk), + accessor_member("client", cgk, n, pc, t). + // AN EXPLICIT INTERFACE IMPLEMENTATION IS THE SECOND WAY A CONCRETE TYPE CAN // ANSWER FOR AN INTERFACE MEMBER, and the clause above cannot see it. // diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 4372d0de..882cc9b7 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -18,13 +18,21 @@ // ============================================================================ .decl dispatch_cap(c0:symbol) +.decl dispatch_closed_world(c0:symbol) .decl jdk_max_depth(c0:number) .decl lib_max_depth(c0:number) .decl runtime_observed_edge(c0:symbol,c1:symbol,c2:symbol) .decl taint_gating(c0:symbol) +.decl accessor_best(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) +.decl accessor_cand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) .decl accessor_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl accessor_edge_count(c0:symbol,c1:symbol,c2:number) +.decl accessor_is_bodiless(c0:symbol,c1:symbol) +.decl accessor_is_explicit(c0:symbol,c1:symbol,c2:symbol) +.decl accessor_member(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl accessor_names_bodiless(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl accessor_site_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl accessor_tier(c0:symbol,c1:symbol,c2:symbol) .decl amqp_bind_var(c0:symbol,c1:symbol,c2:symbol) .decl amqp_binding(c0:symbol,c1:symbol,c2:symbol) @@ -66,6 +74,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) @@ -94,6 +103,7 @@ .decl call_is_unqualified(c0:symbol,c1:symbol) .decl call_kind(c0:symbol,c1:symbol,c2:symbol) .decl call_member_dispatches(c0:symbol,c1:symbol) +.decl call_method_type_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl call_module(c0:symbol,c1:symbol,c2:symbol) .decl call_names_bodiless(c0:symbol,c1:symbol) .decl call_needs_ctor_lookup(c0:symbol,c1:symbol) @@ -135,16 +145,50 @@ .decl client_glued_base(c0:symbol,c1:symbol) .decl client_path(c0:symbol) .decl client_shift(c0:symbol,c1:symbol,c2:number) +.decl collection_initializer(c0:symbol,c1:symbol) +.decl cond_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl conv_act_ok(c0:symbol,c1:symbol,c2:symbol) +.decl conv_action(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl conv_area_fixed(c0:symbol,c1:symbol) +.decl conv_area_ok(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl conv_call(c0:symbol) +.decl conv_ctl_ok(c0:symbol,c1:symbol,c2:symbol) +.decl conv_default(c0:symbol,c1:symbol,c2:symbol) +.decl conv_default_lit(c0:symbol,c1:symbol,c2:symbol) +.decl conv_has(c0:symbol,c1:symbol) +.decl conv_pattern(c0:symbol,c1:symbol) +.decl conv_route(c0:symbol,c1:symbol) +.decl conv_route_call(c0:symbol,c1:number) +.decl conv_s1(c0:symbol,c1:symbol,c2:symbol) +.decl conv_s2(c0:symbol,c1:symbol,c2:symbol) +.decl conv_s2_at(c0:symbol,c1:number) +.decl conv_s2_demand(c0:symbol,c1:symbol) +.decl conv_s2_has(c0:symbol) +.decl conv_s3(c0:symbol,c1:symbol,c2:symbol) +.decl conv_s3_at(c0:symbol,c1:number) +.decl conv_s3_demand(c0:symbol,c1:symbol) +.decl conv_s3_has(c0:symbol) +.decl conv_tok_at(c0:symbol,c1:symbol,c2:number) +.decl conv_tok_span(c0:symbol,c1:symbol,c2:number,c3:number) +.decl conv_use(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl conv_verb(c0:symbol,c1:symbol) .decl conversion_candidate_type(c0:symbol,c1:symbol,c2:symbol) .decl conversion_target_name(c0:symbol,c1:symbol,c2:symbol) +.decl cs_access_outside_assembly_denied(c0:symbol) .decl cs_alias_framework_type(c0:symbol,c1:symbol) .decl cs_amqp_bind(c0:symbol) .decl cs_amqp_publish(c0:symbol) .decl cs_attr_suffix(c0:symbol,c1:symbol) +.decl cs_client_factory_generic(c0:symbol) .decl cs_di_resolve(c0:symbol) +.decl cs_di_resolve_declared(c0:symbol) .decl cs_ef_context_base(c0:symbol) .decl cs_ef_interceptor_register(c0:symbol) .decl cs_ef_save_call(c0:symbol,c1:symbol) +.decl cs_field_access_class(c0:symbol,c1:symbol,c2:symbol) +.decl cs_field_access_count(c0:symbol,c1:symbol,c2:number) +.decl cs_field_access_kind(c0:symbol,c1:symbol,c2:symbol) +.decl cs_field_access_target(c0:symbol,c1:symbol,c2:symbol) .decl cs_framework_base(c0:symbol) .decl cs_framework_callback(c0:symbol,c1:symbol,c2:symbol) .decl cs_framework_callback_prefix(c0:symbol,c1:symbol,c2:symbol) @@ -157,25 +201,43 @@ .decl cs_grpc_client_member_not_rpc(c0:symbol) .decl cs_grpc_client_suffix(c0:symbol) .decl cs_grpc_map_service(c0:symbol) +.decl cs_grpc_runtime_namespace(c0:symbol) .decl cs_grpc_server_suffix(c0:symbol) .decl cs_heritage_in_source(c0:symbol,c1:symbol) +.decl cs_http_api_controller_attr(c0:symbol) +.decl cs_http_area_attr(c0:symbol) +.decl cs_http_area_route(c0:symbol,c1:number,c2:number) .decl cs_http_client_factory_method(c0:symbol) .decl cs_http_client_method(c0:symbol,c1:symbol) .decl cs_http_client_type(c0:symbol) +.decl cs_http_controller_base(c0:symbol) .decl cs_http_controller_suffix(c0:symbol) +.decl cs_http_conventional_route(c0:symbol,c1:number) .decl cs_http_declarative_attr(c0:symbol,c1:symbol) +.decl cs_http_default_route(c0:symbol,c1:symbol) .decl cs_http_map_group(c0:symbol) +.decl cs_http_map_methods(c0:symbol) .decl cs_http_map_verb(c0:symbol,c1:symbol) .decl cs_http_method_member(c0:symbol,c1:symbol) +.decl cs_http_non_action_attr(c0:symbol) .decl cs_http_request_message_type(c0:symbol) .decl cs_http_route_attr(c0:symbol) +.decl cs_http_route_builder_type(c0:symbol) +.decl cs_http_route_value_key(c0:symbol) .decl cs_http_send_message_method(c0:symbol) .decl cs_http_verb_any(c0:symbol) .decl cs_http_verb_attr(c0:symbol,c1:symbol) .decl cs_lower(c0:symbol,c1:symbol) +.decl cs_main_param(c0:symbol) +.decl cs_main_return(c0:symbol) +.decl cs_main_shape(c0:symbol) .decl cs_med_dispatch(c0:symbol,c1:symbol,c2:symbol) .decl cs_med_sender_type(c0:symbol) +.decl cs_member_name_expr(c0:symbol,c1:symbol) .decl cs_mock_arg_matcher(c0:symbol) +.decl cs_mock_behavior_arg(c0:symbol) +.decl cs_mock_factory(c0:symbol,c1:symbol) +.decl cs_mock_new_type(c0:symbol) .decl cs_msg_builder(c0:symbol,c1:symbol) .decl cs_msg_bus_send(c0:symbol) .decl cs_msg_bus_type(c0:symbol,c1:symbol) @@ -192,12 +254,15 @@ .decl cs_remote_confidence_binding(c0:symbol) .decl cs_remote_confidence_exact(c0:symbol) .decl cs_remote_confidence_route_shape(c0:symbol) +.decl cs_remote_confidence_service_name(c0:symbol) .decl cs_remote_transport_grpc(c0:symbol) .decl cs_remote_transport_http(c0:symbol) +.decl cs_route_token_end(c0:symbol) .decl cs_servicebus_handler_event(c0:symbol) .decl cs_string_passthrough(c0:symbol) .decl cs_string_type(c0:symbol) .decl cs_test_attr(c0:symbol) +.decl cs_test_attr_type(c0:symbol) .decl cs_test_lifecycle_attr(c0:symbol) .decl cs_url_not_path_start(c0:symbol) .decl cseg(c0:symbol,c1:number,c2:symbol) @@ -209,6 +274,16 @@ .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_at(c0:symbol,c1:number,c2:symbol) +.decl delegate_arg_expr(c0:symbol,c1:symbol) +.decl delegate_group_demand(c0:symbol,c1:symbol) +.decl delegate_group_method(c0:symbol,c1:symbol,c2:symbol) +.decl delegate_handed_expr(c0:symbol,c1:symbol) +.decl delegate_handed_value(c0:symbol,c1:symbol) +.decl delegate_initializer_name(c0:symbol,c1:symbol,c2:symbol) .decl delegate_invoke_node(c0:symbol,c1:symbol) .decl delegate_member_call(c0:symbol,c1:symbol,c2:symbol) .decl delegate_member_holds(c0:symbol,c1:symbol,c2:symbol) @@ -217,7 +292,15 @@ .decl delegate_member_name(c0:symbol,c1:symbol,c2:symbol) .decl delegate_member_owner(c0:symbol,c1:symbol,c2:symbol) .decl delegate_member_ref(c0:symbol,c1:symbol,c2:symbol) +.decl delegate_param_at(c0:symbol,c1:number,c2:symbol) +.decl delegate_param_call(c0:symbol,c1:symbol) +.decl delegate_param_holds(c0:symbol,c1:symbol) +.decl delegate_param_holds_any(c0:symbol) +.decl delegate_param_node(c0:symbol,c1:symbol) +.decl delegate_param_receives(c0:symbol,c1:symbol) +.decl delegate_param_ref(c0:symbol,c1:symbol) .decl delegate_qualifier_type(c0:symbol,c1:symbol,c2:symbol) +.decl delegate_site_callee(c0:symbol,c1:symbol,c2:number) .decl delegate_store(c0:symbol,c1:symbol,c2:symbol) .decl delegate_stored_expr(c0:symbol,c1:symbol) .decl delegate_storing_op(c0:symbol) @@ -248,6 +331,8 @@ .decl event_remover(c0:symbol,c1:symbol,c2:symbol) .decl event_type_name(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl event_unaccess(c0:symbol,c1:symbol,c2:symbol) +.decl explicit_iface_base(c0:symbol,c1:symbol) +.decl explicit_iface_written(c0:symbol) .decl expr_anon_decl(c0:symbol,c1:symbol,c2:symbol) .decl expr_argument(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_arity(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -284,6 +369,7 @@ .decl expr_owner(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_parent(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl expr_predefined_recv_name(c0:symbol,c1:symbol,c2:symbol) +.decl expr_qualified_field(c0:symbol,c1:symbol,c2:symbol) .decl expr_qualifier_child(c0:symbol,c1:symbol,c2:symbol) .decl expr_receiver_child(c0:symbol,c1:symbol,c2:symbol) .decl expr_recv_is_array(c0:symbol,c1:symbol) @@ -296,6 +382,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) @@ -308,8 +395,11 @@ .decl extension_candidate(c0:symbol,c1:symbol,c2:symbol) .decl extension_candidate_count(c0:symbol,c1:symbol,c2:number) .decl extension_candidate_firm(c0:symbol,c1:symbol,c2:symbol) +.decl extension_candidate_matched(c0:symbol,c1:symbol) .decl extension_in_scope(c0:symbol,c1:symbol,c2:symbol) .decl extension_is_guess(c0:symbol,c1:symbol,c2:symbol) +.decl extension_lib_self_or_ancestor(c0:symbol,c1:symbol) +.decl extension_loose_match(c0:symbol,c1:symbol,c2:symbol) .decl extension_match(c0:symbol,c1:symbol,c2:symbol) .decl extension_method(c0:symbol,c1:symbol,c2:symbol) .decl extension_no_match(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -323,6 +413,7 @@ .decl external_recv_name(c0:symbol,c1:symbol,c2:symbol) .decl external_target(c0:symbol,c1:symbol,c2:symbol) .decl external_type_name(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl field_access(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl field_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl field_in_type(c0:symbol,c1:symbol,c2:symbol) .decl field_initializer(c0:symbol,c1:symbol,c2:symbol) @@ -337,23 +428,58 @@ .decl generated_accessor_read(c0:symbol,c1:symbol,c2:symbol) .decl generated_property(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl generated_property_type(c0:symbol,c1:symbol,c2:symbol) +.decl gm_arg_at(c0:symbol,c1:symbol,c2:number,c3:symbol) +.decl gm_infer_param(c0:symbol,c1:symbol,c2:number) +.decl gm_plain_twin(c0:symbol,c1:symbol) +.decl gm_return_param(c0:symbol,c1:symbol,c2:symbol) +.decl gp_bind_arg(c0:symbol,c1:symbol) +.decl gp_bind_param(c0:symbol,c1:symbol,c2:number) +.decl gp_c_nargs(c0:symbol,c1:number) .decl gp_demand(c0:symbol) +.decl gp_ext_named(c0:symbol,c1:symbol,c2:symbol) +.decl gp_helper_call(c0:symbol,c1:symbol) .decl gp_is_call(c0:symbol) .decl gp_is_local(c0:symbol) +.decl gp_is_param(c0:symbol) +.decl gp_named_call(c0:symbol,c1:symbol,c2:symbol) +.decl gp_named_cand(c0:symbol,c1:symbol) +.decl gp_named_ncand(c0:symbol,c1:number) +.decl gp_param_arg(c0:symbol,c1:symbol) +.decl gp_w_nparams(c0:symbol,c1:number) .decl group_member_substituted(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl group_prefix(c0:symbol,c1:symbol) +.decl grpc_base_at(c0:symbol,c1:symbol) +.decl grpc_cli_ns(c0:symbol,c1:symbol) +.decl grpc_global_using(c0:symbol) +.decl grpc_has_scoped(c0:symbol,c1:symbol) .decl grpc_name_demand(c0:symbol) .decl grpc_name_dot(c0:symbol,c1:number) .decl grpc_name_ends_async(c0:symbol) .decl grpc_name_has_prev(c0:symbol) .decl grpc_name_last_dot(c0:symbol,c1:number) +.decl grpc_name_prefix(c0:symbol,c1:symbol) .decl grpc_name_prev_dot(c0:symbol,c1:number) .decl grpc_name_segs(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_ns_demand(c0:symbol) +.decl grpc_ns_dot(c0:symbol,c1:number) +.decl grpc_ns_not_evidence(c0:symbol) +.decl grpc_ns_prefix(c0:symbol,c1:symbol) +.decl grpc_pair(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl grpc_pair_n(c0:symbol,c1:symbol,c2:number) +.decl grpc_pair_scoped(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_pair_t0(c0:symbol,c1:symbol,c2:symbol) .decl grpc_send_cand(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_send_name(c0:symbol,c1:symbol) +.decl grpc_send_ns_home(c0:symbol,c1:symbol) .decl grpc_send_primary(c0:symbol,c1:symbol,c2:symbol) .decl grpc_send_site(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl grpc_serves(c0:symbol,c1:symbol) +.decl grpc_serves_at(c0:symbol,c1:symbol,c2:symbol) .decl grpc_service_of_name(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_service_type(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_srv_ns(c0:symbol,c1:symbol) +.decl grpc_static_base(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_written_base(c0:symbol,c1:symbol) .decl heritage_has_ctor_args(c0:symbol,c1:symbol,c2:symbol) .decl heritage_of_entity(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl heritage_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -385,8 +511,13 @@ .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_call_count(c0:symbol,c1:symbol,c2:number) +.decl implicit_call_shape(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl implicit_call_target(c0:symbol,c1:symbol,c2: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) @@ -394,7 +525,9 @@ .decl indexer_setter(c0:symbol,c1:symbol,c2:symbol) .decl indexer_write_target(c0:symbol,c1:symbol,c2:symbol) .decl inherited_member_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl initializer_has_assignment(c0:symbol) .decl interface_extends(c0:symbol,c1:symbol,c2:symbol) +.decl interface_extends_tc(c0:symbol,c1:symbol,c2:symbol) .decl interface_has_default_impl(c0:symbol,c1:symbol) .decl interface_member(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl interp_final(c0:symbol,c1:symbol) @@ -405,7 +538,11 @@ .decl interp_step(c0:symbol,c1:number,c2:symbol) .decl invocation_site(c0:symbol,c1:symbol,c2:symbol) .decl invocation_site_classified(c0:symbol,c1:symbol) +.decl lambda_arg_at(c0:symbol,c1:symbol,c2:number,c3:symbol) .decl lambda_assigned_type_ref(c0:symbol,c1:symbol,c2:symbol) +.decl lambda_callee_overloaded(c0:symbol,c1:symbol,c2:symbol) +.decl lambda_callee_param_ref(c0:symbol,c1:symbol,c2:number,c3:symbol) +.decl lambda_callee_single(c0:symbol,c1:symbol) .decl lambda_delegate_in_source(c0:symbol,c1:symbol) .decl lambda_delegate_signature(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl lambda_param_arg_ref(c0:symbol,c1:symbol,c2:symbol) @@ -417,6 +554,7 @@ .decl lc(c0:symbol,c1:symbol) .decl lc_demand(c0:symbol) .decl lc_step(c0:symbol,c1:number,c2:symbol) +.decl lib_member_inaccessible(c0:symbol) .decl lib_type_named(c0:symbol,c1:symbol) .decl local_bind_best_depth(c0:symbol,c1:symbol,c2:number) .decl local_bind_cand(c0:symbol,c1:symbol,c2:symbol,c3:number) @@ -426,6 +564,14 @@ .decl map_call(c0:symbol,c1:symbol) .decl map_group_call(c0:symbol) .decl map_handler(c0:symbol,c1:symbol) +.decl map_handler_at(c0:symbol,c1:number) +.decl map_handler_expr(c0:symbol,c1:symbol) +.decl map_methods_call(c0:symbol) +.decl map_methods_has_verb(c0:symbol) +.decl map_methods_verb(c0:symbol,c1:symbol) +.decl map_verb_call(c0:symbol,c1:symbol) +.decl map_verb_elem(c0:symbol,c1:symbol) +.decl map_verbs_expr(c0:symbol,c1:symbol) .decl marker_scrub(c0:symbol,c1:symbol) .decl mb_demand(c0:symbol) .decl med_arg_clash(c0:symbol,c1:symbol) @@ -458,6 +604,7 @@ .decl member_access(c0:symbol,c1:symbol,c2:symbol) .decl member_access_is_callee(c0:symbol,c1:symbol) .decl member_access_resolved(c0:symbol,c1:symbol) +.decl member_access_site_untyped(c0:symbol,c1:symbol) .decl member_best_rank(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl member_cand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number) .decl member_is_explicit_impl(c0:symbol,c1:symbol) @@ -470,6 +617,9 @@ .decl member_lookup_miss_boundary(c0:symbol,c1:symbol,c2:symbol) .decl member_lookup_miss_real(c0:symbol,c1:symbol,c2:symbol) .decl member_method(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl member_method_newdecl(c0:symbol,c1:symbol,c2:symbol,c3:number) +.decl member_method_newdecl_before(c0:symbol,c1:symbol,c2:symbol,c3:number) +.decl member_method_sig_hidden(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) .decl member_type_substituted(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl method_accepts_argc(c0:symbol,c1:symbol,c2:symbol) .decl method_accepts_extension_argc(c0:symbol,c1:symbol,c2:symbol) @@ -482,6 +632,7 @@ .decl method_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl method_dispatch_candidate(c0:symbol,c1:symbol,c2:symbol) .decl method_explicit_interface(c0:symbol,c1:symbol,c2:symbol) +.decl method_group_handoff(c0:symbol,c1:symbol,c2:symbol) .decl method_has_block(c0:symbol,c1:symbol) .decl method_has_body(c0:symbol,c1:symbol) .decl method_has_params_array(c0:symbol,c1:symbol) @@ -516,6 +667,7 @@ .decl method_owner(c0:symbol,c1:symbol,c2:symbol) .decl method_param_predef(c0:symbol,c1:number,c2:symbol) .decl method_paramc(c0:symbol,c1:symbol,c2:symbol) +.decl method_paramc_any(c0:symbol,c1:symbol) .decl method_partial_def(c0:symbol,c1:symbol) .decl method_partial_impl(c0:symbol,c1:symbol) .decl method_return_external_name(c0:symbol,c1:symbol,c2:symbol) @@ -529,6 +681,11 @@ .decl method_span(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl method_type_arity(c0:symbol,c1:symbol,c2:symbol) .decl method_verb_attr(c0:symbol,c1:symbol,c2:symbol) +.decl mock_ctor(c0:symbol,c1:symbol) +.decl mock_ctor_argc(c0:symbol,c1:symbol) +.decl mock_ctor_behavior(c0:symbol) +.decl mock_ctor_lambda(c0:symbol) +.decl mock_ctor_site(c0:symbol,c1:symbol) .decl module_assembly(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl module_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl module_file(c0:symbol,c1:symbol,c2:symbol) @@ -541,6 +698,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) @@ -571,6 +729,13 @@ .decl msg_request_site(c0:symbol,c1:symbol,c2:symbol) .decl msg_type_arg(c0:symbol,c1:symbol) .decl msg_typed_arg(c0:symbol) +.decl mvc_action(c0:symbol,c1:symbol) +.decl mvc_area(c0:symbol,c1:symbol) +.decl mvc_ctl(c0:symbol,c1:symbol) +.decl mvc_has_area(c0:symbol) +.decl mvc_method_attr_routed(c0:symbol) +.decl mvc_non_action(c0:symbol) +.decl mvc_type_attr_routed(c0:symbol) .decl name_to_type(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl nested_declares(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl new_target_decl_name(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -586,23 +751,36 @@ .decl overrides(c0:symbol,c1:symbol,c2:symbol) .decl overrides_transitive(c0:symbol,c1:symbol,c2:symbol) .decl ovl_arg_at(c0:symbol,c1:symbol,c2:number,c3:symbol) +.decl ovl_arg_iface(c0:symbol,c1:symbol,c2:number,c3:symbol) +.decl ovl_arg_source(c0:symbol,c1:symbol,c2:number,c3:symbol) .decl ovl_arg_type(c0:symbol,c1:symbol,c2:number,c3:symbol) .decl ovl_cand(c0:symbol,c1:symbol,c2:symbol) .decl ovl_conv(c0:symbol,c1:symbol) +.decl ovl_delegate_arity(c0:symbol,c1:number) +.decl ovl_delegate_name(c0:symbol) .decl ovl_drop(c0:symbol,c1:symbol,c2:symbol) .decl ovl_exact(c0:symbol,c1:symbol,c2:symbol) .decl ovl_exact_pos(c0:symbol,c1:symbol,c2:symbol,c3:number) +.decl ovl_ext_receiver(c0:symbol,c1:symbol) .decl ovl_int_suffix(c0:symbol,c1:symbol) .decl ovl_int_suffix2(c0:symbol,c1:symbol) +.decl ovl_lambda_arg(c0:symbol,c1:symbol,c2:number,c3:number) .decl ovl_literal_type(c0:symbol,c1:symbol,c2:symbol) .decl ovl_open_access(c0:symbol) +.decl ovl_param_at(c0:symbol,c1:symbol,c2:symbol,c3:number,c4:symbol) +.decl ovl_param_closed(c0:symbol) +.decl ovl_param_delegate(c0:symbol,c1:symbol) +.decl ovl_param_is_delegate(c0:symbol) .decl ovl_param_type(c0:symbol,c1:number,c2:symbol) .decl ovl_predef(c0:symbol,c1:symbol) .decl ovl_predef_kw(c0:symbol,c1:symbol) .decl ovl_real_suffix(c0:symbol,c1:symbol) +.decl ovl_reflection_type(c0:symbol) .decl ovl_rejected(c0:symbol,c1:symbol,c2:symbol) .decl ovl_site(c0:symbol,c1:symbol) +.decl ovl_source_closed(c0:symbol) .decl ovl_survives(c0:symbol,c1:symbol) +.decl ovl_type_converts(c0:symbol) .decl ovl_visible(c0:symbol,c1:symbol,c2:symbol) .decl ovl_widen(c0:symbol,c1:symbol) .decl p_after_scheme(c0:symbol,c1:symbol) @@ -687,6 +865,7 @@ .decl query_clause_body(c0:symbol,c1:symbol,c2:symbol) .decl query_clause_descending(c0:symbol,c1:symbol) .decl query_clause_source(c0:symbol,c1:symbol,c2:symbol) +.decl recv_value_written_name(c0:symbol,c1:symbol) .decl ref_binds_value(c0:symbol,c1:symbol) .decl ref_denotes(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl ref_denotes_base(c0:symbol,c1:symbol,c2:symbol) @@ -729,6 +908,7 @@ .decl serves_destination(c0:symbol,c1:symbol,c2:symbol) .decl site_in_output(c0:symbol) .decl site_leaves_client(c0:symbol,c1:symbol) +.decl site_unresolved_member(c0:symbol,c1:symbol,c2:symbol) .decl site_unresolved_named(c0:symbol,c1:symbol,c2:symbol) .decl site_unresolved_real(c0:symbol) .decl sseg(c0:symbol,c1:number,c2:symbol) @@ -790,7 +970,9 @@ .decl type_is_static_entity(c0:symbol,c1:symbol) .decl type_is_struct(c0:symbol,c1:symbol) .decl type_is_top_level(c0:symbol,c1:symbol) +.decl type_member_base_unresolved(c0:symbol,c1:symbol,c2:symbol) .decl type_member_name(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_members_incomplete(c0:symbol,c1:symbol) .decl type_modifier(c0:symbol,c1:symbol,c2:symbol) .decl type_module(c0:symbol,c1:symbol,c2:symbol) .decl type_multiple_bases(c0:symbol,c1:symbol) @@ -815,6 +997,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) @@ -840,6 +1023,9 @@ .decl type_struct_heritage_is_class(c0:symbol,c1:symbol,c2:symbol) .decl type_subtype(c0:symbol,c1:symbol,c2:symbol) .decl unbound_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl unresolved_member_access(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl unresolved_member_access_kind(c0:symbol,c1:symbol,c2:symbol) +.decl unresolved_member_accessor(c0:symbol,c1:symbol) .decl url_tmpl(c0:symbol,c1:symbol) .decl using_alias(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl using_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) @@ -849,6 +1035,7 @@ .decl using_is_static(c0:symbol,c1:symbol) .decl using_namespace(c0:symbol,c1:symbol,c2:symbol) .decl using_static_type(c0:symbol,c1:symbol,c2:symbol) +.decl using_var_initializer(c0:symbol) .decl var_block(c0:symbol,c1:symbol,c2:symbol) .decl var_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl var_declared_before(c0:symbol,c1:symbol,c2:symbol) diff --git a/graph/csharp/souffle/export_manifest.tsv b/graph/csharp/souffle/export_manifest.tsv index a48d6138..56f36d4f 100644 --- a/graph/csharp/souffle/export_manifest.tsv +++ b/graph/csharp/souffle/export_manifest.tsv @@ -39,3 +39,6 @@ entry_reachable entry-reachable.csv type_ancestor_type resolution-type-ancestor-type.csv override_pair resolution-virtual-override.csv framework_edge framework-edge.csv +field_access field-access.csv +site_unresolved_member site-unresolved-member.csv +ctor_implicit_type ctor-implicit-type.csv diff --git a/graph/csharp/souffle/gen_decls_all.py b/graph/csharp/souffle/gen_decls_all.py index 4ccb7452..7a8f6cc0 100644 --- a/graph/csharp/souffle/gen_decls_all.py +++ b/graph/csharp/souffle/gen_decls_all.py @@ -69,6 +69,9 @@ # --dispatch-cap K: refuse a virtual-dispatch fan wider than K. Recall-risky, so # opt-in; empty means uncapped, which is the answer that cannot be wrong. "dispatch_cap": ("symbol",), + # --closed-world on: narrow the dispatch fan to constructed types (#473). Staged + # for every language by the shared executor; no C# rule reads it yet. + "dispatch_closed_world": ("symbol",), # --lib-depth: how far a client->lib chain is expanded. Empty means uncapped. "lib_max_depth": ("number",), # The shared executor ALWAYS stages these two, for every language, and emits one diff --git a/graph/java/engine/call-edge-generation/annotation_flow.dl b/graph/java/engine/call-edge-generation/annotation_flow.dl index b4de7727..6174ff01 100644 --- a/graph/java/engine/call-edge-generation/annotation_flow.dl +++ b/graph/java/engine/call-edge-generation/annotation_flow.dl @@ -21,6 +21,15 @@ junit_entry_ann("Test"). junit_entry_ann("Before"). junit_entry_ann("After"). junit_entry_ann("BeforeClass"). junit_entry_ann("AfterClass"). junit_entry_ann("BeforeEach"). junit_entry_ann("AfterEach"). junit_entry_ann("ParameterizedTest"). junit_entry_ann("RepeatedTest"). +junit_entry_ann("TestFactory"). junit_entry_ann("TestTemplate"). + +// A COMPOSED annotation: an annotation type meta-annotated with one of the above is read by the runner as that +// annotation (JUnit 5 `@Target(METHOD) @Test @interface IntegrationCase {}`), transitively. Its own name says +// nothing, so without this a method under it is no entry point (#1418). Only an annotation TYPE is followed: a class +// that carries @Test-named annotations is not one, and an annotation that carries none (`@Audited`) stays out. +junit_entry_ann(a) :- annotation_on(p, m, _, _, t, _), junit_entry_ann(m), + type_decl(p, a, _, _, tc, _, t), annotation_type_category(tc). +annotation_type_category("ANNOTATION_INTERFACE_TYPE"). annotation_type_category("ANNOTATION_TYPE"). http_entry_ann("GET"). http_entry_ann("POST"). http_entry_ann("PUT"). http_entry_ann("DELETE"). http_entry_ann("PATCH"). http_entry_ann("HEAD"). http_entry_ann("OPTIONS"). http_entry_ann("Path"). diff --git a/graph/java/engine/call-edge-generation/callback_dispatch.dl b/graph/java/engine/call-edge-generation/callback_dispatch.dl new file mode 100644 index 00000000..27a2abb7 --- /dev/null +++ b/graph/java/engine/call-edge-generation/callback_dispatch.dl @@ -0,0 +1,93 @@ +// ============================================================================ +// Call-edge generation · A CLIENT OBJECT HANDED TO A LIBRARY, WHICH CALLS IT BACK +// +// verify(repo).save(argThat(new IsValid())); // IsValid implements ArgumentMatcher +// orders.sort(new ByTotal()); // ByTotal implements Comparator +// +// The library runs IsValid.matches / ByTotal.compare, and no call site in client code names +// either method: the call is made inside the library. An override is followed from a call +// on a receiver of the library type (virtual-dispatch.dl), and there is no such receiver +// here, so the named class's methods had no caller at all. The same body written as an +// anonymous class was reached, only because it is lexically inside the caller (`defines`). +// +// THE EDGE. A call site that does NOT resolve to client code, with an argument that is a client +// object created for it (`new C(..)`, or a local initialised with one), reaches each method of C +// that the library can call back: +// (a) an override of a library method (virtual_override from a lib base), when the +// library is staged; or +// (b) with the library absent, a method carrying @Override in a type that has an EXTERNAL +// ancestor (external-types.dl): the annotation says a supertype declares it, and the +// external ancestor is where that supertype can be. +// (c) with the library absent and no @Override written anywhere in C, a public method of a C +// that itself names an external supertype other than a marker interface. +// A helper of a class that marks its overrides is not reached. java.lang.Object's members are +// never fanned (equals / hashCode / toString over every argument is the classic blow-up; +// the same exclusion client_overridden_lib_type makes). +// +// TIER callback_registered, kind "callback": the site HANDS the object over, and the library +// may call it, now or later, or never. It is the tier JavaScript and TypeScript already use +// for `xs.forEach(f)`, labelled so it is never read as a resolved call. The site keeps its +// own row (boundary_lib / ambiguous_unknown) beside it. +// A call that resolves to CLIENT code is left to that code: its body holds the parameter and +// the call on it, and dispatch follows that. An anonymous class is reached as before, by its +// `defines` hop: it is lexically inside the method that hands it over. +// ============================================================================ + +cb_object_member("equals"). cb_object_member("hashCode"). cb_object_member("toString"). +cb_object_member("clone"). cb_object_member("finalize"). + +// an argument of a call that leaves the client, and the client type it holds +cb_arg_type(call, t) :- invocation_site(call, kind), cb_site_kind(kind), + expr_child("client", call, "ARGUMENT", arg), + object_creation_type_resolves("client", arg, _, t). +// … or a local the method created with `new` for it (`ByLength cmp = new ByLength(); xs.sort(cmp)`). +// Only an object CREATED to be handed over: a parameter or a field passed along (`c.accept(w)`) +// is a value flowing through, and the library method it reaches is often not one that calls +// any of its callbacks (a Consumer does not close the AutoCloseable it is given). +cb_arg_type(call, t) :- invocation_site(call, kind), cb_site_kind(kind), + expr_child("client", call, "ARGUMENT", arg), + java_expression(_, "ARGUMENT", _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, arg), + expr_ultimate_method("client", arg, m), + java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), + local_creation_init(local), + expr_type("client", arg, t). +cb_site_kind("method"). cb_site_kind("new"). + +cb_client_site(call) :- client_calls_client("client", call, "client", _). +cb_handoff(call, t) :- cb_arg_type(call, t), !cb_client_site(call), + java_type(_, _, _, _, _, _, _, _, _, _, _, _, _, t). + +// the methods of t, declared in it or inherited from a client ancestor +cb_member(t, name, m) :- cb_handoff(_, t), + java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m). +cb_member(t, name, m) :- cb_handoff(_, t), type_ancestor(t, sup), + java_method(name, _, _, _, _, _, _, sup, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m). + +// (a) an override of a library method +cb_callback(t, m) :- cb_member(t, name, m), !cb_object_member(name), + virtual_override(base, m), method_owner("lib", _, base). +// (b) @Override, under an external ancestor +cb_external_ancestor(t) :- cb_handoff(_, t), type_ancestor(t, x), external_type(x). +cb_callback(t, m) :- cb_member(t, name, m), !cb_object_member(name), cb_external_ancestor(t), + ann_on_method("client", _, "Override", m, _). +// (c) with the library absent and a class that writes no @Override at all, the annotation says +// nothing either way: every PUBLIC instance method it declares is a candidate (a library calls +// an implementation through its interface, and an interface method is public). A class that +// does write @Override has said which methods are overrides, and (b) reads that instead. +cb_writes_override(t) :- cb_handoff(_, t), + java_method(_, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, _, _, _, _, _, m), + ann_on_method("client", _, "Override", m, _). +// Only for a class that names the external supertype ITSELF (`class ByLength implements +// Comparator`), and not a marker interface, which declares nothing to call: an entity +// under a client base that is Serializable would otherwise offer every getter and setter. +// Measured on a 50-file web application before this guard: 20 of 24 such edges were an entity's +// accessors, reached from a test that put the entity in a collection. +cb_marker_type("external:java.io.Serializable"). cb_marker_type("external:Serializable"). +cb_marker_type("external:java.lang.Cloneable"). cb_marker_type("external:Cloneable"). +cb_direct_external(t) :- cb_handoff(_, t), type_parent(t, x), external_type(x), !cb_marker_type(x). +cb_callback(t, m) :- cb_handoff(_, t), cb_direct_external(t), !cb_writes_override(t), + java_method(name, _, _, _, _, _, _, t, _, _, "PUBLIC", _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), + !cb_object_member(name). + +call_chain_edge(call, caller, "-", m, "client", "callback_registered", "callback") :- + cb_handoff(call, t), cb_callback(t, m), call_from(call, caller). diff --git a/graph/java/engine/call-edge-generation/exception_handlers.dl b/graph/java/engine/call-edge-generation/exception_handlers.dl new file mode 100644 index 00000000..63759521 --- /dev/null +++ b/graph/java/engine/call-edge-generation/exception_handlers.dl @@ -0,0 +1,86 @@ +// ============================================================================ +// Call-edge generation · EXCEPTION HANDLERS (a built exception -> the handler it runs) +// +// throw new OrderNotFound(id); // written here +// repo.find(id).orElseThrow(OrderNotFound::new); // or here +// @RestControllerAdvice class Errors { +// @ExceptionHandler(OrderNotFound.class) ResponseEntity missing(OrderNotFound e) { … } +// } // run from there +// +// When an exception leaves a request handler, the web framework looks up the handler +// method declared for the exception's type and runs it to build the response: one in +// the same controller, or one in an advice class, which serves every controller. No +// call site names the handler, so without this file `impact` on a handler listed no +// dependent, `path '*' ` found nothing, and a change to the exception's +// handling looked local to the advice class. +// +// WHY A CALL EDGE WITH TIER event_dispatch. It is the shape event_dispatch.dl and +// entity_lifecycle.dl already model: one JVM, keyed on a TYPE, the handler running in +// the same request before the response is written. The KIND column, `exception_handler`, +// tells it apart from an application event or an entity callback. The constructor row +// of the `new` itself is kept: the edge is ADDED beside it. +// +// THE KEY IS THE BUILT EXCEPTION'S TYPE, with the subtype rule the framework applies: a +// handler for OrderNotFound runs for an ExpressOrderNotFound, and a handler for the +// subtype does not run for the base. The handled types are the classes the annotation +// names, or, when it names none, the handler's parameter type. +// +// THE SITE IS WHERE THE EXCEPTION IS BUILT: `new X(..)` and the `X::new` handed to an +// orElseThrow. That is where a change in handling is felt, and a project exception type +// is built to be thrown. +// +// WHAT IS NOT MODELLED, and stays unlinked rather than guessed: +// - a handler for a LIBRARY type (Exception, RuntimeException, a validation or +// framework exception): the framework or a library throws those, not a client +// `new`, and every `new IllegalStateException` in the project would reach it. Only a +// type the project declares is a key; +// - whether the building method runs under a request at all: a job that throws the +// same type is linked too, which is the static answer to "which handler would take +// this"; +// - which of two matching handlers is closer: both are linked; +// - an advice's basePackages / assignableTypes / annotations filter: it is read as +// serving every controller. +// ============================================================================ + +// ── the handlers, and the types each one takes ────────────────────────────── +exh_ann_method(m, a) :- ann_on_method(_, a, n, m, _), cfg_exh_ann(n), method_owner("client", _, m). +exh_names_classes(m) :- exh_ann_method(m, a), + ann_arg(_, a, arg, _, "CLASS_REFERENCE", _), cfg_exh_arg(arg). +exh_handles(m, t) :- exh_ann_method(m, a), + ann_arg(_, a, arg, v, "CLASS_REFERENCE", _), cfg_exh_arg(arg), + config_class_ref("annotation", a, v, t, "client"). +exh_handles(m, t) :- exh_ann_method(m, _), !exh_names_classes(m), + event_param_type(m, t), java_type(_, _, _, _, _, _, _, _, _, _, _, _, _, t). + +// a handler of an advice class serves every controller; any other serves its own type +exh_global(m) :- exh_ann_method(m, _), method_owner("client", t, m), + ann_on_type(_, _, n, t), cfg_exh_advice_ann(n). + +// ── where a project exception is built ────────────────────────────────────── +exh_built(call, caller, t) :- + java_expression("OBJECT_CREATION", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, call), + expr_type("client", call, t), call_from(call, caller). +exh_built(mr, caller, t) :- + java_expression("METHOD_REFERENCE", _, _, _, _, _, _, _, _, _, _, "CONSTRUCTOR", _, _, _, _, _, _, _, _, _, _, _, _, mr), + java_expression(_, "QUALIFIER", _, _, _, _, mr, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, q), + expr_type("client", q, t), call_from(mr, caller). +exh_built(mr, caller, t) :- + java_expression("METHOD_REFERENCE", _, _, _, _, _, _, _, _, _, _, "CONSTRUCTOR", _, _, _, _, _, _, _, _, _, _, _, _, mr), + method_ref_qualifier_resolves("client", mr, _, t), call_from(mr, caller). + +// the handler takes the built type itself, or an ancestor of it +exh_handles_built(m, t) :- exh_handles(m, t). +exh_handles_built(m, bt) :- exh_built(_, _, bt), type_ancestor(bt, t), exh_handles(m, t). + +// ── the edge ──────────────────────────────────────────────────────────────── +exh_edge(call, caller, m) :- exh_built(call, caller, bt), exh_handles_built(m, bt), + exh_global(m), caller != m. +exh_edge(call, caller, m) :- exh_built(call, caller, bt), exh_handles_built(m, bt), + !exh_global(m), caller != m, + method_owner("client", ht, m), method_owner("client", ht, caller). +exh_edge(call, caller, m) :- exh_built(call, caller, bt), exh_handles_built(m, bt), + !exh_global(m), caller != m, + method_owner("client", ht, m), method_owner("client", ct, caller), type_ancestor(ct, ht). + +call_chain_edge(call, caller, "-", m, "client", "event_dispatch", "exception_handler") :- + exh_edge(call, caller, m). diff --git a/graph/java/engine/config-resolution/entry-points.dl b/graph/java/engine/config-resolution/entry-points.dl index 98c0535e..0cad1c7b 100644 --- a/graph/java/engine/config-resolution/entry-points.dl +++ b/graph/java/engine/config-resolution/entry-points.dl @@ -47,10 +47,57 @@ cfg_registered_type(t, kind) :- xml_container_class(kind, t, _). cfg_registered_type(t, "config_handler") :- config_class_ref(fmt, k, _, t, "client"), fmt != "annotation", cfg_key_expects_class(k). +// … a type an ANNOTATION registers: @WebServlet / @WebFilter / @WebListener, exactly as +// their web.xml twins; a JPA @Converter; a JAX-RS @Provider (knobs.dl (6b)). +cfg_registered_type(t, kind) :- ann_on_type("client", _, n, t), cfg_registering_type_ann(n, kind). +// … a class an annotation ARGUMENT names, for the arguments that mean "instantiate this": +// @EntityListeners(Audit.class), @Convert(converter = PriceConverter.class). Keyed on the +// annotation AND the argument, so an arbitrary @Foo(Bar.class) still registers nothing. +cfg_registered_type(t, kind) :- ann_arg(prov, a, arg, _, "CLASS_REFERENCE", _), + cfg_registering_class_arg(n, arg, kind), cfg_ann_name_of(prov, a, n), + cfg_ann_class_arg(prov, a, arg, t), type_decl("client", _, _, _, _, _, t). +cfg_ann_name_of(prov, a, n) :- ann_on_type(prov, a, n, _). +cfg_ann_name_of(prov, a, n) :- ann_on_field(prov, a, n, _, _). +cfg_ann_name_of(prov, a, n) :- ann_on_method(prov, a, n, _, _). +// … a class registered IN CODE with the servlet container, by a class literal or an +// instance: ctx.addServlet("orders", OrderServlet.class), ctx.addFilter("a", new F()), +// ctx.addListener(L.class). The receiver has to be typed as a registrar (knobs.dl (6b)). +cfg_registered_type(t, kind) :- cfg_servlet_registration(name, kind), call_site(call, name, recv), + servlet_registrar_recv(recv), call_arg(call, _, arg), registered_arg_type(arg, t). +// … and Spring Boot's new ServletRegistrationBean<>(new OrderServlet(), "/orders"). +cfg_registered_type(t, kind) :- cfg_servlet_registration_bean(bn, kind), + object_creation_named(e, bn), expr_child("client", e, "ARGUMENT", arg), + registered_arg_type(arg, t). +object_creation_named(e, bn) :- java_expression("OBJECT_CREATION", _, _, _, _, _, _, _, _, _, v, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + cfg_servlet_registration_bean(bn, _), v = bn. +object_creation_named(e, bn) :- java_expression("OBJECT_CREATION", _, _, _, _, _, _, _, _, _, v, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + cfg_servlet_registration_bean(bn, _), v = cat(bn, "<>"). +// the type an argument hands over: X.class, or new X(..) +registered_arg_type(arg, t) :- java_expression("CLASS_LITERAL", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, q, _, _, _, _, _, _, arg), + java_type(_, q, _, _, _, _, _, _, _, _, _, _, _, t). +registered_arg_type(arg, t) :- object_creation_type_resolves("client", arg, _, t). +// a receiver typed as ServletContext (or a Jetty handler), whether the type is staged as a +// library or known only by its written name +servlet_registrar_recv(recv) :- recv_type_simple(recv, n), cfg_servlet_registrar_type(n). +// recv_type_simple(Expr, SimpleName): an expression typed by a library or external type of +// that simple name, for the few framework names a rule asks about (typed_simple_demand). +typed_simple_demand(n) :- cfg_servlet_registrar_type(n). +typed_simple_demand(n) :- cfg_jaxrs_client_owner(n). +recv_type_simple(e, n) :- typed_simple_demand(n), lib_type(n, _, _, _, _, _, _, _, _, _, _, _, _, t), + expr_type("lib", e, t). +recv_type_simple(e, n) :- typed_simple_demand(n), x = cat("external:", n), expr_type("external", e, x). +recv_type_simple(e, n) :- typed_simple_demand(n), expr_type("external", e, x), + strlen(x) > strlen(n) + 1, + substr(x, max(0, strlen(x) - strlen(n) - 1), min(strlen(n) + 1, strlen(x))) = cat(".", n). + // (1) callbacks on a registered type — by name … config_entry_point(m, kind, t) :- cfg_registered_type(t, kind), java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), cfg_container_callback_name(name). +// … and by the per-kind name list (a JPA converter's convertTo*, a JAX-RS provider's filter). +config_entry_point(m, kind, t) :- cfg_registered_type(t, kind), + java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), + cfg_registered_callback_name(kind, name). config_entry_point(m, "spring_factories", t) :- cfg_registered_type(t, "spring_factories"), java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), cfg_factories_callback_name(name). @@ -71,8 +118,37 @@ config_entry_point(m, kind, t) :- cfg_registered_type(t, kind), config_entry_point(m, kind, t) :- xml_lifecycle_method(t, mname, kind), method_name_in_type(mname, t, m), method_owner("client", _, m). +// (3b) the annotation twin of (3): @Bean(initMethod = "start", destroyMethod = "stop") +// names `start` and `stop` on the type the @Bean method builds. +config_entry_point(m, kind, fm) :- cfg_factory(prov, a, fm, _), ann_arg(prov, a, arg, mname, _, _), + cfg_bean_lifecycle_arg(arg, kind), mname != "", cfg_factory_type(fm, t), + method_name_in_type(mname, t, m), method_owner("client", _, m). + // (4) annotation-driven container callbacks that annotation_flow.dl does not cover. config_entry_point(m, "lifecycle", m) :- ann_on_method(_, _, n, m, _), cfg_lifecycle_ann(n). +// JPA entity callbacks, on an entity, a mapped superclass or an @EntityListeners class. +// Only those: the provider never runs a @PrePersist on any other class, so a callback +// annotation on a class no entity names is not an entry point (it stays unreachable, and +// call-edge-generation/entity_lifecycle.dl gives it no caller either). +config_entry_point(m, "orm_hook", m) :- ann_on_method(_, _, n, m, _), cfg_jpa_callback_ann(n), + method_owner("client", t, m), jpa_callback_owner(t). +jpa_callback_owner(t) :- ann_on_type("client", _, n, t), cfg_entity_ann(n). +jpa_callback_owner(t) :- ann_arg(prov, a, arg, _, "CLASS_REFERENCE", _), cfg_ann_name_of(prov, a, n), + cfg_entity_listeners_ann(n), cfg_registering_class_arg(n, arg, _), cfg_ann_class_arg(prov, a, arg, t). +// A JAX-RS @QueryParam / @PathParam / … parameter of a client type T: the runtime builds +// it with T.valueOf(String), T.fromString(String) or new T(String). +jaxrs_param_type(t) :- ann_on_param(_, _, n, p, _), cfg_jaxrs_param_ann(n), + method_param_type_resolves("client", p, _, t). +config_entry_point(m, "framework_hook", t) :- jaxrs_param_type(t), + java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, _, "1", _, _, _, m), + cfg_jaxrs_param_factory(name), string_param_0(m). +config_entry_point(m, "framework_hook", t) :- jaxrs_param_type(t), + method_kind("client", "CONSTRUCTOR", _, m), method_owner("client", t, m), + java_method(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, "1", _, _, _, m), + string_param_0(m). +string_param_0(m) :- java_method_parameter(_, "0", m, _, tn, _, _, _, _, _, _, _, _), + jaxrs_string_type(tn). +jaxrs_string_type("String"). jaxrs_string_type("java.lang.String"). config_entry_point(m, "scheduled", m) :- ann_on_method(_, _, n, m, _), cfg_scheduled_ann(n). config_entry_point(m, "queue", m) :- ann_on_method(_, _, n, m, _), cfg_listener_ann(n). // … and an ApplicationListener implementation's onApplicationEvent, which Spring calls @@ -115,6 +191,21 @@ config_entry_point(m, kind, t) :- cfg_stub_base(base, kind), java_method(name, _, _, _, _, _, _, base, _, _, _, _, _, _, _, _, _, pc, _, _, _, _), name != "", java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", pc, _, _, _, m). +// ...and when protoc ran at build time, the ImplBase is an external type with no declared methods +// to test an override against. The service's own rpc-shaped methods are the handlers +// (framework-behavior/destinations.dl, grpc_ext_handler; #1548). +config_entry_point(m, "grpc_service", t) :- grpc_ext_handler(t, _, _), + java_method(_, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), + grpc_rpc_shaped(m). + +// (6b) A REGISTERED type's method marked @Override whose supertype is known only by its +// written name (the framework API is not in the analysed library set): it overrides the +// framework's declaration, so the framework calls it. A JAX-RS ContainerRequestFilter's +// filter(..), an ExceptionMapper's toResponse(..). +config_entry_point(m, kind, t) :- cfg_registered_type(t, kind), + type_parent(t, x), external_type(x), + ann_on_method(_, _, "Override", m, t), + method_kind("client", "INSTANCE_METHOD", _, m). // (7) A REGISTERED type's overrides, generalised. cfg_registered_type covers web.xml // and config-named handlers; clause (1) matched a knob name list or a LIBRARY supertype. diff --git a/graph/java/engine/config-resolution/knobs.dl b/graph/java/engine/config-resolution/knobs.dl index 2ec0de97..51bb3376 100644 --- a/graph/java/engine/config-resolution/knobs.dl +++ b/graph/java/engine/config-resolution/knobs.dl @@ -52,6 +52,66 @@ cfg_props_ann("ConfigurationProperties"). // ── (6) CONTAINER-INVOKED METHODS (entry points sourced from annotations that // carry a config VALUE — the name-only cases stay in annotation_flow.dl) ── cfg_lifecycle_ann("PostConstruct"). cfg_lifecycle_ann("PreDestroy"). +// @Bean(initMethod = "start", destroyMethod = "stop"): the annotation twin of the XML +// init-method / destroy-method attributes (8), naming a method on the bean's type. +// cfg_bean_lifecycle_arg(AnnotationArgument, EntryKind) +cfg_bean_lifecycle_arg("initMethod", "lifecycle_init"). +cfg_bean_lifecycle_arg("destroyMethod", "lifecycle_destroy"). +// JPA entity callbacks: the persistence provider calls them on the entity, on its mapped +// superclasses and on the classes its @EntityListeners names, with no call site anywhere. +cfg_jpa_callback_ann("PrePersist"). cfg_jpa_callback_ann("PostPersist"). +cfg_jpa_callback_ann("PreUpdate"). cfg_jpa_callback_ann("PostUpdate"). +cfg_jpa_callback_ann("PreRemove"). cfg_jpa_callback_ann("PostRemove"). +cfg_jpa_callback_ann("PostLoad"). + +// ── (6b) TYPES A FRAMEWORK INSTANTIATES BECAUSE AN ANNOTATION NAMES THEM ────── +// Only these. Lifting the "annotation" exclusion on cfg_registered_type wholesale would +// register every X.class written in any annotation argument. +// cfg_registering_type_ann(AnnotationOnTheType, EntryKind): the type itself carries it. +cfg_registering_type_ann("WebServlet", "web_servlet"). +cfg_registering_type_ann("WebFilter", "web_filter"). +cfg_registering_type_ann("WebListener", "web_listener"). +cfg_registering_type_ann("Converter", "orm_hook"). // JPA @Converter +cfg_registering_type_ann("Provider", "framework_hook"). // JAX-RS @Provider +// cfg_registering_class_arg(Annotation, Argument, EntryKind): a class named in an argument. +cfg_registering_class_arg("EntityListeners", "value", "orm_hook"). +cfg_registering_class_arg("Convert", "converter", "orm_hook"). +// Methods a framework calls on a type of that kind whose supertype is usually not in the +// analysed library set (so "overrides a library method" finds nothing), matched by name. +// cfg_registered_callback_name(EntryKind, MethodName) +cfg_registered_callback_name("orm_hook", "convertToDatabaseColumn"). +cfg_registered_callback_name("orm_hook", "convertToEntityAttribute"). +cfg_registered_callback_name("framework_hook", "filter"). // Container{Request,Response}Filter +cfg_registered_callback_name("framework_hook", "toResponse"). // ExceptionMapper +cfg_registered_callback_name("framework_hook", "readFrom"). // MessageBodyReader +cfg_registered_callback_name("framework_hook", "isReadable"). +cfg_registered_callback_name("framework_hook", "writeTo"). // MessageBodyWriter +cfg_registered_callback_name("framework_hook", "isWriteable"). +cfg_registered_callback_name("framework_hook", "getSize"). +cfg_registered_callback_name("framework_hook", "aroundReadFrom"). // ReaderInterceptor +cfg_registered_callback_name("framework_hook", "aroundWriteTo"). // WriterInterceptor +cfg_registered_callback_name("framework_hook", "getContext"). // ContextResolver +cfg_registered_callback_name("framework_hook", "getConverter"). // ParamConverterProvider +// A JAX-RS request parameter whose type is not a String is built by that type's static +// valueOf(String) / fromString(String) or its String constructor. +cfg_jaxrs_param_ann("QueryParam"). cfg_jaxrs_param_ann("PathParam"). +cfg_jaxrs_param_ann("HeaderParam"). cfg_jaxrs_param_ann("FormParam"). +cfg_jaxrs_param_ann("MatrixParam"). cfg_jaxrs_param_ann("CookieParam"). +cfg_jaxrs_param_factory("valueOf"). cfg_jaxrs_param_factory("fromString"). +// Registration in code: ServletContext.addServlet("name", X.class), addFilter(.., new F()), +// addListener(L.class), and Jetty's handler.addServlet(X.class, "/p"). The receiver's type +// decides, because addListener is a common method name. +// cfg_servlet_registration(MethodName, EntryKind) +cfg_servlet_registration("addServlet", "web_servlet"). +cfg_servlet_registration("addFilter", "web_filter"). +cfg_servlet_registration("addListener", "web_listener"). +cfg_servlet_registrar_type("ServletContext"). +cfg_servlet_registrar_type("ServletContextHandler"). +cfg_servlet_registrar_type("ServletHandler"). +// … and Spring Boot's registration beans, which take the instance as a constructor argument. +cfg_servlet_registration_bean("ServletRegistrationBean", "web_servlet"). +cfg_servlet_registration_bean("FilterRegistrationBean", "web_filter"). +cfg_servlet_registration_bean("ServletListenerRegistrationBean", "web_listener"). cfg_scheduled_ann("Scheduled"). cfg_listener_ann("EventListener"). cfg_listener_ann("KafkaListener"). cfg_listener_ann("TransactionalEventListener"). @@ -70,6 +130,9 @@ cfg_xml_class_elem("listener-class"). cfg_xml_class_elem("resource-class"). cfg_xml_class_elem("provider-class"). cfg_xml_class_elem("handler-class"). cfg_xml_class_elem("ejb-class"). cfg_xml_class_elem("mapper-class"). // (c) the element that DEFINES a bean (its class comes from cfg_xml_class_attr) +// a document root under which `#{...}` is a bound statement parameter, not a Spring +// expression (MyBatis mapper XML; xml-wiring.dl) +cfg_xml_param_marker_root("mapper"). cfg_xml_bean_elem("bean"). cfg_xml_bean_elem("alias"). // ── (8) XML: attributes naming a METHOD the container invokes ──────────────── @@ -83,6 +146,10 @@ cfg_xml_lifecycle_attr("factory-method", "lifecycle_factory"). cfg_xml_container_elem("servlet-class", "web_servlet"). cfg_xml_container_elem("filter-class", "web_filter"). cfg_xml_container_elem("listener-class", "web_listener"). +// The that ties a to its . +cfg_xml_servlet_name_elem("servlet-name"). +cfg_xml_servlet_mapping_elem("servlet-mapping"). +cfg_xml_url_pattern_elem("url-pattern"). // ── (10) XML: wiring elements (a bean depends on another bean) ─────────────── // cfg_xml_wire_elem(Tag, WireKind) @@ -140,6 +207,15 @@ cfg_stub_base_suffix("Skeleton", "rpc_service"). // older RPC generators cfg_stub_client_suffix("BlockingStub"). cfg_stub_client_suffix("FutureStub"). cfg_stub_client_suffix("Stub"). +cfg_stub_client_suffix("BlockingV2Stub"). // grpc-java 1.73+, newBlockingV2Stub + +// With the generated class absent (#1548), the holder is known only by its written name, `Grpc`, +// and an rpc only by its response observer parameter. +.decl cfg_grpc_holder_suffix(c0:symbol) +cfg_grpc_holder_suffix("Grpc"). +.decl cfg_grpc_observer_type(c0:symbol) +cfg_grpc_observer_type("StreamObserver"). +cfg_grpc_observer_type("io.grpc.stub.StreamObserver"). // ── (8) DESTINATIONS: a name that identifies a place a message or request goes ── // A producer and the consumer that serves it live in different deployables and never @@ -234,6 +310,32 @@ cfg_sends_verb("delete", "DELETE"). cfg_sends_verb("patchForObject", "PAT cfg_sends_verb("get", "GET"). cfg_sends_verb("post", "POST"). cfg_sends_verb("patch", "PATCH"). +// The servlet method that serves each verb, for a servlet mapped to a URL pattern by +// web.xml or @WebServlet. `service` handles every verb. +cfg_servlet_verb_method("doGet", "GET"). cfg_servlet_verb_method("doPost", "POST"). +cfg_servlet_verb_method("doPut", "PUT"). cfg_servlet_verb_method("doDelete", "DELETE"). +cfg_servlet_verb_method("doHead", "HEAD"). cfg_servlet_verb_method("doOptions", "OPTIONS"). +cfg_servlet_verb_method("doPatch", "PATCH"). cfg_servlet_verb_method("service", "any"). +cfg_servlet_url_arg("value"). cfg_servlet_url_arg("urlPatterns"). +// a JAX-RS method with @Path and no verb is a sub-resource locator +cfg_subresource_locator_ann("Path"). + +// The JAX-RS client API: client.target(url).path(p).request(..).get(..). The chain has to +// start on one of these types, typed as the library's (never a client class of the same +// name), AND pass through target(..) and request(..). +cfg_jaxrs_client_owner("Client"). cfg_jaxrs_client_owner("WebTarget"). +cfg_jaxrs_client_factory("ClientBuilder"). // ClientBuilder.newClient().target(..) +cfg_jaxrs_invoke_verb("get", "GET"). cfg_jaxrs_invoke_verb("post", "POST"). +cfg_jaxrs_invoke_verb("put", "PUT"). cfg_jaxrs_invoke_verb("delete", "DELETE"). +cfg_jaxrs_invoke_verb("head", "HEAD"). cfg_jaxrs_invoke_verb("options", "OPTIONS"). +cfg_jaxrs_invoke_verb("method", "any"). +// builder steps between request(..) and the verb +cfg_jaxrs_builder_step("accept"). cfg_jaxrs_builder_step("acceptLanguage"). +cfg_jaxrs_builder_step("acceptEncoding"). cfg_jaxrs_builder_step("header"). +cfg_jaxrs_builder_step("headers"). cfg_jaxrs_builder_step("cookie"). +cfg_jaxrs_builder_step("cacheControl"). cfg_jaxrs_builder_step("property"). +cfg_jaxrs_builder_step("async"). cfg_jaxrs_builder_step("rx"). + // (i) an annotation that makes a TYPE a declarative HTTP CLIENT. Its methods carry the // same mapping annotations a controller does, so without this a Feign interface is // read as a handler and answers "who serves this route". @@ -243,6 +345,9 @@ cfg_client_type_ann("RegisterRestClient"). // MicroProfile // ── (10) PERSISTENCE: a query is a reference to a schema ──────────────────── cfg_entity_ann("Entity"). cfg_entity_ann("MappedSuperclass"). cfg_query_ann("NamedQuery"). cfg_query_ann("NamedNativeQuery"). +// the same declarations in a mapping file (META-INF/orm.xml): the element carries the name +// attribute and a child holding the text. +cfg_orm_named_query_elem("named-query"). cfg_orm_named_query_elem("named-native-query"). // the EntityManager-like receivers that make a call a query, for the same reason a // broker send is gated on its template: `createQuery` is not a rare method name. @@ -257,6 +362,13 @@ cfg_query_method("createNativeQuery", "inline"). // and there is no call site anywhere (Spring Data, and Micronaut Data's @Query). cfg_method_query_ann("Query"). cfg_method_query_ann("NativeQuery"). cfg_query_arg("value"). cfg_query_arg("query"). +// the subject keywords a Spring Data DERIVED query method name opens with; the criteria +// follow the first `By` after it (framework-behavior/persistence.dl). +cfg_derived_query_prefix("find"). cfg_derived_query_prefix("read"). +cfg_derived_query_prefix("get"). cfg_derived_query_prefix("query"). +cfg_derived_query_prefix("search"). cfg_derived_query_prefix("stream"). +cfg_derived_query_prefix("count"). cfg_derived_query_prefix("exists"). +cfg_derived_query_prefix("delete"). cfg_derived_query_prefix("remove"). // ── (15) CONDITIONS: an annotation that decides whether a bean EXISTS ──────── // A bean the container did not create has no methods that run, so a condition is // the difference between a live filter chain and a dead one. Three families, kept @@ -401,3 +513,17 @@ cfg_entity_callback_ann("PreUpdate", "update"). cfg_entity_callback_ann("Post cfg_entity_callback_ann("PreRemove", "remove"). cfg_entity_callback_ann("PostRemove", "remove"). // a type annotation whose class arguments are listener classes cfg_entity_listeners_ann("EntityListeners"). + +// ── (22) EXCEPTION HANDLERS: a built exception runs the @ExceptionHandler for it ── +// A web framework hands an exception that leaves a request handler to the handler +// method declared for its type: one in the same controller, or one in an advice class +// for every controller. No call site names the handler +// (call-edge-generation/exception_handlers.dl). +// +// cfg_exh_ann(Name): a method annotation that makes the method an exception handler +// for the types named in cfg_exh_arg, or, with none named, for its parameter's type. +cfg_exh_ann("ExceptionHandler"). +cfg_exh_arg("value"). cfg_exh_arg("exception"). +// cfg_exh_advice_ann(Name): a type annotation whose handlers serve every controller. +cfg_exh_advice_ann("ControllerAdvice"). +cfg_exh_advice_ann("RestControllerAdvice"). diff --git a/graph/java/engine/config-resolution/xml-wiring.dl b/graph/java/engine/config-resolution/xml-wiring.dl index b24da4a8..53223a2b 100644 --- a/graph/java/engine/config-resolution/xml-wiring.dl +++ b/graph/java/engine/config-resolution/xml-wiring.dl @@ -103,8 +103,15 @@ xml_prop_value_key(ownerElem, propName, key, default) :- cfg_placeholder(v, key, default). // A SpEL expression in XML is a declared blind spot: `#{...}` can name anything. +// Not in a document whose ROOT element says `#{...}` means something else: in a MyBatis +// , `#{id}` is a bound parameter, a property of the statement's parameter object, +// and nothing Spring evaluates (cfg_xml_param_marker_root). The parser classifies every +// `#{...}` as SPEL_EXPRESSION whatever the document, and on a mapper-heavy project those +// rows outnumbered every real unknown. +xml_param_marker_file(f) :- xml_elem("client", tag, _, _, "", f, _), cfg_xml_param_marker_root(tag). config_unresolved("xml", vh, expr, "spel_expression") :- - xml_vref("client", expr, "SPEL_EXPRESSION", _, _, _, vh). + xml_vref("client", expr, "SPEL_EXPRESSION", _, owner, _, vh), + xml_elem("client", _, _, _, _, f, owner), !xml_param_marker_file(f). // ───────────────────────────────────────────────────────────────────────────── // 4. CONTAINER REGISTRATIONS (web.xml and friends) 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/expression-resolution/overload.dl b/graph/java/engine/expression-resolution/overload.dl index 0e918839..e312a933 100644 --- a/graph/java/engine/expression-resolution/overload.dl +++ b/graph/java/engine/expression-resolution/overload.dl @@ -299,6 +299,41 @@ arg_value_type(call, pos, vt) :- java_expression(_, "ARGUMENT", _, _, _, _, call arg_value_type(call, pos, vt) :- java_expression(_, "ARGUMENT", _, _, _, _, call, pos, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, arg), expr_ultimate_type("client", arg, encl), java_field(name, vt, _, _, _, _, _, _, encl, _, _, _, _, _), value_type_name(vt). +// An ARITHMETIC or COMPARISON expression argument: `new Elements(elements.size() - 1)`, `remove(--i)`, `put(k, a == b)`. +// Its operands may be untyped (a call into an unstaged library), but the operator fixes what kind of value it is: +// `-` `*` `/` `%` and the shifts make a number, a comparison or `&&` / `||` / `!` a boolean. `+` is left out (a String +// concatenation is a `+` too), and so are `&` `|` `^` (numeric or boolean by operand). "numeric" is a value type of its +// own because the width is not known: it is compatible with every numeric primitive and wrapper, never with String, +// boolean or a reference type. Without it the call kept `Elements(Element...)` beside `Elements(int)`, and every +// caller of the one was a caller of the other. +// java_expression (25): 1 kind 2 edgeRole 7 parentExpressionHash 8 position 14 operatorString +.decl numeric_op(o:symbol) +.decl numeric_unary_op(o:symbol) +.decl boolean_op(o:symbol) +.decl numeric_prim(p:symbol) +.decl prim_value(v:symbol) +numeric_op("-"). numeric_op("*"). numeric_op("/"). numeric_op("%"). numeric_op("<<"). numeric_op(">>"). numeric_op(">>>"). +numeric_unary_op("-"). numeric_unary_op("+"). numeric_unary_op("++"). numeric_unary_op("--"). numeric_unary_op("~"). +boolean_op("=="). boolean_op("!="). boolean_op("<"). boolean_op(">"). boolean_op("<="). boolean_op(">="). boolean_op("&&"). boolean_op("||"). +arg_value_type(call, pos, "numeric") :- java_expression("BINARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + numeric_op(op). +arg_value_type(call, pos, "numeric") :- java_expression("UNARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + numeric_unary_op(op). +arg_value_type(call, pos, "boolean") :- java_expression("BINARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + boolean_op(op). +arg_value_type(call, pos, "boolean") :- java_expression("UNARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, "!", _, _, _, _, _, _, _, _, _, _, _). +numeric_prim("int"). numeric_prim("long"). numeric_prim("float"). numeric_prim("double"). +numeric_prim("short"). numeric_prim("byte"). numeric_prim("char"). +value_compatible("numeric", b) :- value_prim(b, p), numeric_prim(p). +// A primitive value (a literal, a primitive variable, an arithmetic or comparison expression) never reaches a +// parameter typed as a CLIENT type: boxing makes a java.lang wrapper, which no project type is. +prim_value(v) :- value_prim(v, v), v != "String", v != "null". +prim_value("numeric"). +value_mismatch(call, callee) :- expr_call_candidate(call, callee), + arg_value_type(call, pos, at), prim_value(at), + param_type_hash(callee, pos, pt), type_prov(pt, "client"), + !arg_is_target_typed(call, pos). + // Parameter value-type (parameterTypeName col 4 is the simple name: int / String / ...). param_value_type(callee, pos, vt) :- java_method_parameter(_, pos, callee, _, vt, _, _, _, _, _, _, _, _), value_type_name(vt). param_value_type(callee, pos, vt) :- lib_method_parameter(_, pos, callee, _, vt, _, _, _, _, _, _, _, _), value_type_name(vt). diff --git a/graph/java/engine/framework-behavior/destinations.dl b/graph/java/engine/framework-behavior/destinations.dl index 3c21a940..a91ba983 100644 --- a/graph/java/engine/framework-behavior/destinations.dl +++ b/graph/java/engine/framework-behavior/destinations.dl @@ -117,13 +117,13 @@ route_tail_declared(a) :- ann_on_method(_, a, n, _, _), cfg_route_ann(n), cfg_ro // then answers "who serves this route" with somebody else's outbound call. route_type_is_client(t) :- ann_on_type(_, _, n, t), cfg_client_type_ann(n). -route_raw_of_method(m, path, "server") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), +route_raw_own(m, path, "server") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), cfg_route_ann(n), cfg_route_server_ann(n), !route_type_is_client(owner), - route_base(owner, base), path = cat(base, tail). -route_raw_of_method(m, path, "client") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), - cfg_route_ann(n), !cfg_route_server_ann(n), route_base(owner, base), path = cat(base, tail). -route_raw_of_method(m, path, "client") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), - cfg_route_ann(n), route_type_is_client(owner), route_base(owner, base), path = cat(base, tail). + route_base(owner, base), path_join(base, tail, path). +route_raw_own(m, path, "client") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), + cfg_route_ann(n), !cfg_route_server_ann(n), route_base(owner, base), path_join(base, tail, path). +route_raw_own(m, path, "client") :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), + cfg_route_ann(n), route_type_is_client(owner), route_base(owner, base), path_join(base, tail, path). // A handler that names only its VERB still has a path: its resource class's. The // verb annotation contributes no remainder of its own, so the route is the base and // nothing more, which is what JAX-RS means by @GET on a method under @Path("/orders"). @@ -131,13 +131,91 @@ route_raw_of_method(m, path, "client") :- route_tail(m, tail), ann_on_method(_, // its remainder from the @Path, and without the guard it would ALSO produce a second, // shorter route at the bare class path. method_route_ann(m) :- ann_on_method(_, _, n, m, _), cfg_route_ann(n). -route_raw_of_method(m, base, "server") :- ann_on_method(_, _, n, m, owner), +route_raw_own(m, base, "server") :- ann_on_method(_, _, n, m, owner), cfg_route_verb_ann(n, _), !method_route_ann(m), !route_type_is_client(owner), route_base(owner, base). // … and the same shape on a declarative client interface (MicroProfile @RegisterRestClient). -route_raw_of_method(m, base, "client") :- ann_on_method(_, _, n, m, owner), +route_raw_own(m, base, "client") :- ann_on_method(_, _, n, m, owner), cfg_route_verb_ann(n, _), !method_route_ann(m), route_type_is_client(owner), route_base(owner, base). +route_raw_of_method(m, p, side) :- route_raw_own(m, p, side), method_owner("client", owner, m), + !locator_only_resource(owner). + +// ── joining the two halves: exactly one '/' between them ──────────────────── +// JAX-RS and Spring both insert the separator when they concatenate a class path and a +// method path, and drop a doubled one: @Path("/items") + @Path("{id}") is /items/{id}, +// and @RequestMapping("/api/") + @GetMapping("/x") is /api/x. A bare cat() gave +// /items{id} and /api//x, neither of which any client writes. +path_join_demand(base, tail) :- route_tail(m, tail), ann_on_method(_, _, n, m, owner), + cfg_route_ann(n), route_base(owner, base). +path_join(b, t, t) :- path_join_demand(b, t), b = "". +path_join(b, t, b) :- path_join_demand(b, t), b != "", t = "". +path_join(b, t, j) :- path_join_demand(b, t), path_ends_slash(b), path_starts_slash(t), + j = cat(b, substr(t, 1, strlen(t) - 1)). +path_join(b, t, j) :- path_join_demand(b, t), path_ends_slash(b), path_starts_other(t), + j = cat(b, t). +path_join(b, t, j) :- path_join_demand(b, t), path_ends_other(b), path_starts_slash(t), + j = cat(b, t). +path_join(b, t, j) :- path_join_demand(b, t), path_ends_other(b), path_starts_other(t), + j = cat(b, cat("/", t)). +path_ends_slash(b) :- path_join_demand(b, _), strlen(b) > 0, substr(b, strlen(b) - 1, 1) = "/". +path_starts_slash(t) :- path_join_demand(_, t), strlen(t) > 0, substr(t, 0, 1) = "/". +// the negative halves are stated positively: the locator rules feed path_join_demand from +// a route that path_join itself produced, so a negation here would be a cycle through it +path_ends_other(b) :- path_join_demand(b, _), strlen(b) > 0, substr(b, strlen(b) - 1, 1) != "/". +path_starts_other(t) :- path_join_demand(_, t), strlen(t) > 0, substr(t, 0, 1) != "/". + +// ── JAX-RS SUB-RESOURCE LOCATORS ──────────────────────────────────────────── +// A method with @Path and no verb returns the object that serves the rest of the request: +// @Path("/widgets") class Widgets { @Path("/{id}") Widget find(..) { .. } } +// class Widget { @GET String details() { .. } } +// Widget.details serves GET /widgets/{id}. The locator's declared return type is the +// resource; its verb methods take the locator's route, and its own @Path methods extend +// it. One level only: a resource that returns its own type would otherwise grow the +// route without end. +jaxrs_locator(m, r) :- ann_on_method(_, _, n, m, owner), cfg_subresource_locator_ann(n), + !method_verb_declared(m), !route_type_is_client(owner), + method_return_type_resolves("client", m, _, r), r != owner. +locator_base(r, raw) :- jaxrs_locator(m, r), route_raw_own(m, raw, "server"). +// A class with no @Path of its own that a locator returns is not a root resource: its +// methods are served only below the locator, never at their own bare path. +locator_only_resource(r) :- jaxrs_locator(_, r), !route_base_declared(r). +route_raw_of_method(m, raw, "server") :- locator_base(r, raw), ann_on_method(_, _, n, m, r), + cfg_route_verb_ann(n, _), !method_route_ann(m). +route_raw_of_method(m, p, "server") :- locator_base(r, raw), route_tail(m, tail), + ann_on_method(_, _, n, m, r), cfg_route_ann(n), cfg_route_server_ann(n), path_join(raw, tail, p). +path_join_demand(raw, tail) :- locator_base(r, raw), route_tail(m, tail), ann_on_method(_, _, _, m, r). + +// ── SERVLETS MAPPED TO A URL PATTERN ──────────────────────────────────────── +// web.xml maps a servlet to its patterns through the both elements carry: +// wapp.W +// w/widgets +// and @WebServlet("/widgets") / urlPatterns = {..} says the same on the class. The servlet's +// do* methods serve the pattern, each for its own verb. +servlet_decl_name(t, name, fp) :- xml_container_class("web_servlet", t, elem), + xml_elem("client", _, _, _, parent, fp, elem), + xml_elem("client", nt, _, raw, parent, _, _), cfg_xml_servlet_name_elem(nt), + cfg_clean(raw, name), name != "". +servlet_mapped_pattern(name, pat, fp) :- xml_elem("client", mt, _, _, _, fp, mp), cfg_xml_servlet_mapping_elem(mt), + xml_elem("client", nt, _, rawn, mp, _, _), cfg_xml_servlet_name_elem(nt), cfg_clean(rawn, name), + xml_elem("client", pt, _, rawp, mp, _, _), cfg_xml_url_pattern_elem(pt), cfg_clean(rawp, pat). +servlet_pattern(t, pat) :- servlet_decl_name(t, name, fp), servlet_mapped_pattern(name, pat, fp). +servlet_pattern(t, pat) :- ann_on_type("client", a, "WebServlet", t), cfg_servlet_url_arg(arg), + ann_arg("client", a, arg, pat, "STRING_LITERAL", _). +// An exact pattern is the route. A path-prefix pattern "/widgets/*" serves every path +// below /widgets, which the join can only express as a template: /widgets/{path} meets a +// client that concatenates "/widgets/" + id by route shape, never exactly. The catch-all +// "/*", the default servlet "/" and an extension pattern "*.do" name no route. +servlet_route_raw(t, pat) :- servlet_pattern(t, pat), !contains("*", pat), pat != "/", pat != "". +servlet_route_raw(t, r) :- servlet_pattern(t, pat), strlen(pat) > 2, + substr(pat, strlen(pat) - 2, 2) = "/*", pre = substr(pat, 0, strlen(pat) - 2), + !contains("*", pre), r = cat(pre, "/{path}"). +servlet_handler(m, t, v) :- servlet_route_raw(t, _), + java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), + cfg_servlet_verb_method(name, v). +route_declared(raw) :- servlet_route_raw(_, raw). +serves_destination(m, "http", u) :- servlet_handler(m, t, _), servlet_route_raw(t, raw), url_path(raw, u). + route_declared(raw) :- route_raw_of_method(_, raw, _). // A declared route goes through the same normalisation as a client's URL, so // @GetMapping("pets/visits") and a client's "…/pets/visits?petId={id}" meet. @@ -232,7 +310,13 @@ sends_destination_at(call, m, tr, d) :- sends_via(call, tr), call_site(call, nam // (d) an imperative HTTP call. The URL is routinely built by concatenation, so the // destination is any path-shaped string written anywhere inside the URL argument. -expr_descendant(a, a) :- sends_via(c, "http"), call_arg(c, _, a). +// Only the URL argument. A RestTemplate send takes (url, request body, ..): reading every +// argument made a body literal ("shipped") a destination of its own, which linked the +// call to whatever handler served /shipped and, as a second fragment, cost it the route +// it really calls. Every method in cfg_sends_http takes the URL first. +http_url_arg(c, a) :- sends_via(c, "http"), call_site(c, name, _), cfg_sends_http(name), + call_arg(c, "0", a). +expr_descendant(a, a) :- http_url_arg(_, a). expr_descendant(a, d) :- expr_descendant(a, c), expr_child("client", c, _, d). // Real code builds the URL into a local and passes the local: // String url = base + "/api/x/" + id + "/detail"; @@ -244,8 +328,7 @@ expr_descendant(a, d) :- expr_descendant(a, c), expr_reference_name(c, n), java_local_variable(n, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, lv), expr_owner("client", lv, _, d). -http_arg_literal(v) :- sends_via(call, "http"), call_site(call, name, _), cfg_sends_http(name), - call_arg(call, _, arg), expr_descendant(arg, d), expr_kind("client", "LITERAL", _, d), +http_arg_literal(v) :- http_url_arg(_, arg), expr_descendant(arg, d), expr_kind("client", "LITERAL", _, d), expr_literal("client", v, d). // A URL literal is a destination in its own right. Without this the only client paths // that resolved were the ones that happened to be spelled identically to a declared @@ -295,15 +378,43 @@ url_path(s, u) :- url_no_scheme(s, p), url_no_query(p, q), strlen(q) > 0, url_path(s, u) :- url_no_scheme(s, p), url_no_query(p, q), strlen(q) > 0, substr(q, 0, 1) != "/", u = cat("/", q). -sends_destination_at(call, m, "http", u) :- sends_via(call, "http"), call_site(call, name, _), - cfg_sends_http(name), call_arg(call, _, arg), expr_descendant(arg, d), +sends_destination_at(call, m, "http", u) :- http_url_arg(call, arg), expr_descendant(arg, d), arg_destination(d, p), url_path(p, u), expr_ultimate_method("client", call, m). sends_destination(m, tr, u) :- sends_destination_at(_, m, tr, u). +// (e) the JAX-RS client: client.target("http://shop/orders").path("{id}").request().get(..). +// The URL is built across the chain, so it is followed from target(..) through each +// path(..) to request(..) and the verb that sends it. Kept apart from sends_via: the +// verb methods (put, delete) share names with RestTemplate's URL-first sends, and +// reading put(Entity.json(x))'s argument as a URL is the defect (d) above avoids. +// The chain root is declared as the JAX-RS Client / WebTarget, and that name does not resolve +// to a class of the project (a project's own `Client` with a target(..) method is not the +// API), or is typed as the library's; or the chain starts on ClientBuilder itself. +jaxrs_client_root(root) :- recv_type_simple(root, n), cfg_jaxrs_client_owner(n). +jaxrs_client_root(root) :- recv_declared_simple(root, n), cfg_jaxrs_client_owner(n), + !expr_type("client", root, _). +jaxrs_client_root(root) :- expr_reference_name(root, n), cfg_jaxrs_client_factory(n), + !simple_name_declared(n). +jaxrs_target(call, d) :- call_site(call, "target", recv), recv_root(recv, root), + jaxrs_client_root(root), call_arg(call, "0", arg), arg_destination(arg, d). +jaxrs_target(call, j) :- call_site(call, "path", recv), jaxrs_target(recv, base), + call_arg(call, "0", arg), expr_kind("client", "LITERAL", _, arg), + expr_literal("client", v, arg), path_join(base, v, j). +path_join_demand(base, v) :- call_site(call, "path", recv), jaxrs_target(recv, base), + call_arg(call, "0", arg), expr_kind("client", "LITERAL", _, arg), expr_literal("client", v, arg). +dest_raw(v) :- call_site(call, "target", _), call_arg(call, "0", arg), + expr_kind("client", "LITERAL", _, arg), expr_literal("client", v, arg). +jaxrs_builder(call, d) :- call_site(call, "request", tgt), jaxrs_target(tgt, d). +jaxrs_builder(call, d) :- call_site(call, n, inner), cfg_jaxrs_builder_step(n), jaxrs_builder(inner, d). +jaxrs_send(call, m, d, v) :- call_site(call, n, b), cfg_jaxrs_invoke_verb(n, v), jaxrs_builder(b, d), + expr_ultimate_method("client", call, m). +path_candidate(d) :- jaxrs_send(_, _, d, _). +sends_destination_at(call, m, "http", u) :- jaxrs_send(call, m, d, _), url_path(d, u). + // EVERY path fragment this one call writes. A URL built by concatenation leaves more // than one — "/api/inventory/" + sku + "/reserved" leaves two — and a prefix rule that // looks at only the first will answer the route that the second one rules out. -send_fragment(call, f) :- sends_via(call, "http"), call_arg(call, _, arg), +send_fragment(call, f) :- http_url_arg(call, arg), expr_descendant(arg, d), expr_kind("client", "LITERAL", _, d), expr_literal("client", raw, d), url_path(raw, f). // NOTE: deliberately NOT gated on http_path_shaped. That guard wants two segments, @@ -321,7 +432,7 @@ route_contradicted(call, s) :- send_fragment(call, f), serves_destination(_, "ht // where they are written, the FIRST is the route's literal prefix and the LAST is its // tail, and the segment counts must add up — which is what keeps // /api/x/{id}/reserve away from /api/x/{id}/reservation. -send_frag_at(call, f, key) :- sends_via(call, "http"), call_arg(call, _, arg), +send_frag_at(call, f, key) :- http_url_arg(call, arg), expr_descendant(arg, d), expr_kind("client", "LITERAL", _, d), expr_literal("client", raw, d), url_path(raw, f), expr_src_pos("client", d, line, col), @@ -428,6 +539,8 @@ method_verb(m, v) :- ann_on_method(_, _, n, m, _), cfg_route_verb_ann(n, v). method_verb_declared(m) :- ann_on_method(_, _, n, m, _), cfg_route_verb_ann(n, _). method_verb(m, "any") :- ann_on_method(_, _, n, m, _), cfg_route_ann(n), !cfg_route_verb_known(n), !method_verb_declared(m). cfg_route_verb_known(n) :- cfg_route_verb(n, _). +// a servlet's doGet serves GET, its service(..) every verb +method_verb(m, v) :- servlet_handler(m, _, v). // … the client's from the call it made, or from its own exchange annotation. call_verb(call, v) :- call_site(call, name, _), cfg_sends_verb(name, v). // a fluent chain names the verb one call earlier: .get().uri(path) @@ -436,6 +549,8 @@ call_verb(call, v) :- call_site(call, _, recv), call_site(recv, name, _), cfg_se send_verb(m, v) :- sends_via(call, "http"), expr_ultimate_method("client", call, m), call_verb(call, v). send_verb(m, "any") :- sends_destination(m, "http", _), !send_verb_known(m). send_verb_known(m) :- sends_via(call, "http"), expr_ultimate_method("client", call, m), call_verb(call, _). +send_verb(m, v) :- jaxrs_send(_, m, _, v). +send_verb_known(m) :- jaxrs_send(_, m, _, _). send_verb(m, v) :- route_of_method(m, _, "client"), method_verb(m, v). // ── gRPC: the generated stub and the handler it serves (#1108) ────────────────────────────── @@ -484,6 +599,89 @@ remote_edge_via(call, from, to, "grpc", key, conf) :- grpc_sends_at(call, from, // the stub's type resolved, so the pairing rests on a type rather than a name alone conf = "exact". +// ── gRPC when the generated Grpc holder is NOT in the source tree (#1548) ──────────────── +// protoc usually runs at build time, into target/ or build/, so neither the ImplBase nor the stub +// has a java_type row and the rules above never fire. Both ends are still EXTERNAL types +// (resolution/external-types.dl) whose names carry the holder as written: `extends +// OrderServiceGrpc.OrderServiceImplBase` and a field of type `OrderServiceGrpc.OrderServiceBlockingStub`. +// These clauses feed grpc_serves and grpc_sends_at, so the edge rule above is shared. +// +// The shape is checked, not just the suffix: the holder is `Grpc` and the nested type is `` +// plus a server or client suffix, the same on both. An external `RequestStub` or `Foo.BarStub` +// is not a generated stub. +// +// The key is the QUALIFIED holder, read per file. The external node is shared by every file that +// wrote the same name, and one real 2,800-file project imports `MetricsServiceGrpc` from three +// packages (two versions of one API and an unrelated one): keyed on the simple name, each client +// would be paired with all three services. A written package qualifier, then the file's +// single-type import of the holder, then the file's own package when it has no on-demand import, +// which is the order javac resolves the name in. A holder only an on-demand import could supply +// is left unpaired. +grpc_ext_name_demand(x, w) :- external_type(x), strlen(x) > 9, + w = substr(x, 9, strlen(x) - 9), contains(".", w), + grpc_generated_suffix(k), strlen(w) > strlen(k), + substr(w, max(0, strlen(w) - strlen(k)), min(strlen(k), strlen(w))) = k. +grpc_generated_suffix(k) :- cfg_stub_client_suffix(k). +grpc_generated_suffix(k) :- cfg_stub_base_suffix(k, "grpc_service"), !cfg_grpc_holder_suffix(k). +grpc_ext_dot(w, i) :- grpc_ext_name_demand(_, w), i = range(0, strlen(w)), substr(w, i, 1) = ".". +grpc_ext_last_dot(w, l) :- grpc_ext_dot(w, _), l = max i : grpc_ext_dot(w, i). +grpc_ext_prev_dot(w, p) :- grpc_ext_last_dot(w, l), grpc_ext_dot(w, j), j < l, + p = max i : { grpc_ext_dot(w, i), i < l }. +grpc_ext_has_prev(w) :- grpc_ext_prev_dot(w, _). +// grpc_ext_parts(ExternalType, Qualifier, Holder, Nested): `a.b.FooGrpc.FooStub` -> a.b, FooGrpc, FooStub +grpc_ext_parts(x, pre, h, n) :- grpc_ext_name_demand(x, w), grpc_ext_prev_dot(w, p), grpc_ext_last_dot(w, l), + pre = substr(w, 0, p), h = substr(w, p + 1, l - p - 1), n = substr(w, l + 1, strlen(w) - l - 1). +grpc_ext_parts(x, "", h, n) :- grpc_ext_name_demand(x, w), !grpc_ext_has_prev(w), grpc_ext_last_dot(w, l), + h = substr(w, 0, l), n = substr(w, l + 1, strlen(w) - l - 1). +grpc_ext_service(x, s, n) :- grpc_ext_parts(x, _, h, n), cfg_grpc_holder_suffix(hs), + strlen(h) > strlen(hs), substr(h, max(0, strlen(h) - strlen(hs)), min(strlen(hs), strlen(h))) = hs, + s = substr(h, 0, max(0, strlen(h) - strlen(hs))). +grpc_ext_server(x) :- grpc_ext_service(x, s, n), cfg_stub_base_suffix(k, "grpc_service"), n = cat(s, k). +grpc_ext_client(x) :- grpc_ext_service(x, s, n), cfg_stub_client_suffix(k), n = cat(s, k). + +// the service class that extends the external ImplBase, and the file that wrote the extends +grpc_ext_impl_site(t, x, fp) :- + java_type_reference(_, "SUPER_TYPE", t, _, _, _, _, "0", _, _, _, _, _, _, _, _, _, ref), + type_ref_external(ref, x), grpc_ext_server(x), + java_type(_, _, _, _, _, _, _, fp, _, _, _, _, _, t). +// a call on a receiver typed as an external stub, and the file of the method that makes it +grpc_ext_send_site(call, from, x, fp, name) :- call_site(call, name, recv), name != "", + expr_type("external", recv, x), grpc_ext_client(x), + expr_ultimate_method("client", call, from), + java_method(_, _, _, _, fp, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, from). + +grpc_ext_site(x, fp) :- grpc_ext_impl_site(_, x, fp). +grpc_ext_site(x, fp) :- grpc_ext_send_site(_, _, x, fp, _). +grpc_file_on_demand(fp) :- grpc_ext_site(_, fp), java_import(_, _, _, _, fp, _, "false", "true", _, _, _). +grpc_file_package(fp, pkg) :- grpc_ext_site(_, fp), + java_type(n, q, _, _, _, _, "TOP_LEVEL_PLACEMENT", fp, _, _, _, _, _, _), + strlen(q) > strlen(n), pkg = substr(q, 0, max(0, strlen(q) - strlen(n) - 1)). +grpc_file_package(fp, "") :- grpc_ext_site(_, fp), + java_type(n, n, _, _, _, _, "TOP_LEVEL_PLACEMENT", fp, _, _, _, _, _, _). +// grpc_ext_holder_q(ExternalType, File, QualifiedHolder) +grpc_ext_holder_q(x, fp, q) :- grpc_ext_site(x, fp), grpc_ext_parts(x, pre, h, _), pre != "", + q = cat(pre, cat(".", h)). +grpc_ext_holder_q(x, fp, q) :- grpc_ext_site(x, fp), grpc_ext_parts(x, "", h, _), + java_import("SINGLE_TYPE", q, _, h, fp, _, "false", _, _, _, _). +grpc_ext_holder_q(x, fp, q) :- grpc_ext_site(x, fp), grpc_ext_parts(x, "", h, _), + !type_has_explicit_import(h, fp), !grpc_file_on_demand(fp), + grpc_file_package(fp, pkg), pkg != "", q = cat(pkg, cat(".", h)). +grpc_ext_holder_q(x, fp, h) :- grpc_ext_site(x, fp), grpc_ext_parts(x, "", h, _), + !type_has_explicit_import(h, fp), !grpc_file_on_demand(fp), grpc_file_package(fp, ""). + +// An rpc method takes the response StreamObserver: (request, observer) for unary and server +// streaming, (observer) for client and bidi streaming. With the ImplBase absent there is no +// declaration to test an override against, and a helper written beside the rpcs has no observer. +grpc_rpc_shaped(m) :- java_method_parameter(_, _, m, bt, _, _, _, _, _, _, _, _, _), cfg_grpc_observer_type(bt). + +grpc_ext_handler(t, x, fp) :- grpc_ext_impl_site(t, x, fp). +grpc_ext_handler(t, x, fp) :- grpc_ext_impl_site(t0, x, fp), type_ancestor(t, t0). +grpc_serves(m, key) :- grpc_ext_handler(t, x, fp), grpc_ext_holder_q(x, fp, q), + java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, "INSTANCE_METHOD", _, _, _, _, m), + name != "", grpc_rpc_shaped(m), key = cat(q, cat("/", name)). +grpc_sends_at(call, from, key) :- grpc_ext_send_site(call, from, x, fp, name), + grpc_ext_holder_q(x, fp, q), key = cat(q, cat("/", name)). + // (a) exact — a topic, a queue, or a route both ends spell the same way. remote_edge_via(call, from, to, tr, d, "exact") :- sends_destination_at(call, from, tr, d), serves_destination(to, tr, d), from != to, tr != "http". diff --git a/graph/java/engine/framework-behavior/persistence.dl b/graph/java/engine/framework-behavior/persistence.dl index ae33a78d..6ffd4dd1 100644 --- a/graph/java/engine/framework-behavior/persistence.dl +++ b/graph/java/engine/framework-behavior/persistence.dl @@ -47,6 +47,17 @@ jpa_entity_field(t, fname, f) :- jpa_entity(t), type_ancestor(t, sup), // type with its own arguments, so no special handling of the container is needed. jpa_named_query(name, text, t) :- ann_on_type("client", a, n, t), cfg_query_ann(n), ann_arg("client", a, "name", name, _, _), ann_arg("client", a, "query", text, _, _). +// … and in a mapping file: `…` in +// META-INF/orm.xml (or any mapping file the unit lists). The parser already holds the +// element, its name attribute and the child's text; the declaring "type" is the element, +// since a mapping-file query belongs to the persistence unit rather than to a class. The +// JPQL is the child's text exactly as it is for the annotation's `query` argument, so the +// entity and property rules below read both the same way. A is SQL, +// which names tables rather than entities, and is left to report no_entity_in_query. +orm_named_query(name, text, nq) :- xml_elem("client", tag, _, _, _, _, nq), cfg_orm_named_query_elem(tag), + xml_attr("client", "name", raw, nq, _), cfg_clean(raw, name), name != "", + xml_elem("client", "query", _, text, nq, _, _), text != "". +jpa_named_query(name, text, nq) :- orm_named_query(name, text, nq). // ── uses: em.createNamedQuery("…") / em.createQuery("…") ──────────────────── // Gated on the receiver's declared type, for the same reason a broker send is: the @@ -99,6 +110,75 @@ persistence_query(m, "inline", "-", text) :- query_call(_, m, "inline", text). persistence_query(m, "spring_data", "-", text) :- ann_on_method("client", a, n, m, _), cfg_method_query_ann(n), cfg_query_arg(arg), ann_arg("client", a, arg, text, _, _). +// ── Spring Data: a DERIVED query, where the method NAME is the query ──────── +// `List findByColorAndLabelStartingWith(String c, String p)` on an interface +// extending a repository base carries no text at all: the framework parses the name at +// start-up, and a property the name spells that the entity does not have fails there. +// So renaming Widget.color breaks findByColor exactly as it breaks an @Query naming +// `w.color`, and it is recorded the same way: persistence_query (kind "derived", the +// method name as its text), persistence_entity, and one persistence_field per property. +// +// The entity is the repository base's FIRST type argument (`JpaRepository`), +// read from the written extends clause because the base is normally not staged; the +// base is recognised by the same suffix knob that makes the interface a proxy bean +// (cfg_repository_base_suffix, di.dl). The entity need not carry @Entity: a document +// store's repository derives its queries the same way. +// +// The NAME: a subject prefix (cfg_derived_query_prefix: find, read, get, query, search, +// stream, count, exists, delete, remove), anything up to the first `By` after it +// (`Distinct`, `Top10`, `All`), then the criteria. A property is matched where a +// criterion can start (right after that `By`, an `And`, an `Or` or an `OrderBy`) as +// the capitalised property name, followed by the end of the name, `_` or another +// capital (an operator such as `StartingWith`, the next `And`, a nested path). Where +// two properties match at one place the LONGER one is the property (`labelText` over +// `label`), as Spring's own parser resolves it. A method carrying @Query is not +// derived: the annotation's text wins, and the rule above already reads it. +repo_entity(repo, e) :- type_super_ref_h(repo, sn, sref), cfg_repo_name_matches(sn), + type_decl(_, _, _, _, "INTERFACE_TYPE", _, repo), + java_type_reference(_, _, _, _, _, sref, "0", "1", _, _, _, _, _, _, _, _, _, argRef), + type_ref_resolves(_, argRef, e), java_type(_, _, _, _, _, _, _, _, _, _, _, _, _, e). +repo_entity_field(e, fname, f) :- repo_entity(_, e), + java_field(fname, _, _, _, _, _, _, _, e, _, _, _, _, f). +repo_entity_field(e, fname, f) :- repo_entity(_, e), type_ancestor(e, sup), + java_field(fname, _, _, _, _, _, _, _, sup, _, _, _, _, f). +// the capitalised property name, as it is spelled inside a method name +repo_field_cap(e, cap, f) :- repo_entity_field(e, fname, f), strlen(fname) > 0, + cfg_upper(substr(fname, 0, 1), up), cap = cat(up, substr(fname, 1, strlen(fname) - 1)). +repo_field_cap(e, fname, f) :- repo_entity_field(e, fname, f), strlen(fname) > 0, + cfg_is_upper_char(substr(fname, 0, 1)). + +has_method_query_ann(m) :- ann_on_method("client", _, n, m, _), cfg_method_query_ann(n). +derived_candidate(m, name, e, p) :- repo_entity(repo, e), + java_method(name, _, _, _, _, _, _, repo, _, _, _, _, _, _, _, _, "ABSTRACT_METHOD", _, _, _, _, m), + cfg_derived_query_prefix(p), strlen(name) > strlen(p) + 2, + substr(name, 0, strlen(p)) = p, !has_method_query_ann(m). +derived_by_at(m, i) :- derived_candidate(m, name, _, p), + i = range(strlen(p), strlen(name) - 1), substr(name, i, 2) = "By". +derived_criteria(m, e, crit) :- derived_candidate(m, name, e, _), + b = min i : derived_by_at(m, i), b + 2 < strlen(name), + crit = substr(name, b + 2, strlen(name) - b - 2). +// where a criterion may start: the beginning, or after And / Or / By (OrderBy), at a capital +derived_start(m, 0) :- derived_criteria(m, _, _). +derived_start(m, j) :- derived_criteria(m, _, crit), derived_joiner(w), + j = range(1, strlen(crit)), j >= strlen(w), substr(crit, j - strlen(w), strlen(w)) = w, + cfg_is_upper_char(substr(crit, j, 1)). +derived_joiner("And"). derived_joiner("Or"). derived_joiner("By"). +// a property spelled at a start, and ending where a property name can end +derived_match(m, j, n, f) :- derived_start(m, j), derived_criteria(m, e, crit), + repo_field_cap(e, cap, f), n = strlen(cap), j + n <= strlen(crit), + substr(crit, j, n) = cap, derived_ends(crit, j + n). +derived_ends(crit, k) :- derived_criteria(_, _, crit), k = strlen(crit). +derived_ends(crit, k) :- derived_criteria(_, _, crit), k = range(1, strlen(crit)), + cfg_is_upper_char(substr(crit, k, 1)). +derived_ends(crit, k) :- derived_criteria(_, _, crit), k = range(1, strlen(crit)), + substr(crit, k, 1) = "_". +derived_longest(m, j, n) :- derived_match(m, j, _, _), n = max l : derived_match(m, j, l, _). + +persistence_query(m, "derived", "-", name) :- derived_criteria(m, _, _), + derived_candidate(m, name, _, _). +persistence_entity(m, e) :- derived_criteria(m, e, _). +pf_derived(m, f) :- derived_longest(m, j, n), derived_match(m, j, n, f). + // ── what the query text names ─────────────────────────────────────────────── // No JPQL parser: an entity is recognised where a query can introduce one, and the // candidate set is only the @Entity types, so the test is both cheap and specific. @@ -108,7 +188,7 @@ persistence_query(m, "spring_data", "-", text) :- ann_on_method("client", a, n, // start, in the middle and at the end. Both delimiters are required: measured on a real // codebase, matching " FROM Pet" without the trailing space also matched // " FROM PetType", and the blast radius of PetType.name then included Pet.name. -query_padded(text, p) :- persistence_query(_, _, _, text), p = cat(" ", cat(text, " ")). +query_padded(text, p) :- persistence_query(_, kind, _, text), kind != "derived", p = cat(" ", cat(text, " ")). query_names_type(text, name) :- query_padded(text, p), jpa_entity(t), java_type(name, _, _, _, _, _, _, _, _, _, _, _, _, t), jpa_intro(kw), contains(cat(" ", cat(kw, cat(name, " "))), p). @@ -117,7 +197,7 @@ jpa_intro("JOIN "). jpa_intro("join "). jpa_intro("Join "). jpa_intro("UPDATE "). jpa_intro("update "). jpa_intro("Update "). jpa_intro("INTO "). jpa_intro("into "). -persistence_entity(m, t) :- persistence_query(m, _, _, text), query_names_type(text, name), +persistence_entity(m, t) :- persistence_query(m, kind, _, text), kind != "derived", query_names_type(text, name), java_type(name, _, _, _, _, _, _, _, _, _, _, _, _, t), jpa_entity(t). // A property is named with a dot after its alias — `o.customerId`. The dot is what @@ -141,6 +221,7 @@ pf_nested(m, f) :- persistence_entity(m, t), persistence_query(m, _, _, text), mid != fname, jpa_after(d), contains(cat(".", cat(mid, cat(".", cat(fname, d)))), p). // A property read directly SOMEWHERE is direct, even if it also appears nested. +pf_any(m, f) :- pf_derived(m, f). pf_direct(m, f) :- pf_any(m, f), !pf_nested(m, f). persistence_field(m, f, "direct") :- pf_direct(m, f). persistence_field(m, f, "nested") :- pf_nested(m, f), !pf_direct(m, f). diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl index c60de8a6..46427be8 100644 --- a/graph/java/engine/resolution/external-types.dl +++ b/graph/java/engine/resolution/external-types.dl @@ -78,6 +78,23 @@ name_resolves_in_file(tn, fp) :- ref_file(_, tn, fp), type_resolves_java_lang(_, // on an inferred local got an `external:var.` boundary beside its real target (3,842 of 85,025 // edges on a 1,360-file project). Its type is the initializer's: local-flow.dl carries a client // one, and the `var` clause of expr_type below an external one. #1401. +// +// A TYPE VARIABLE is not a type name either. A field written `protected M mapper` in `Base` +// arrives as a CLASS reference named `M`, which nothing declares, so a call through it became a +// library boundary on `external:M` (#1412). The variable is in scope from its declaring type, a +// type enclosing that, or the generic method whose signature or body writes it. +ref_names_tvar(ref) :- java_type_reference(_, _, encl, _, _, _, _, "0", tn, _, _, _, _, _, _, _, _, ref), + tn != "", java_type_parameter(tn, _, _, _, _, _, encl, _). +ref_names_tvar(ref) :- java_type_reference(_, _, encl, _, _, _, _, "0", tn, _, _, _, _, _, _, _, _, ref), + tn != "", type_in_type("client", encl, outer), java_type_parameter(tn, _, _, _, _, _, outer, _). +ref_names_tvar(ref) :- java_type_reference(_, _, _, _, _, _, _, "0", tn, _, _, _, _, _, _, m, "METHOD", ref), + tn != "", java_method_type_parameter(tn, _, _, _, _, _, _, m, _, _). +ref_names_tvar(ref) :- java_type_reference(_, _, _, _, _, _, _, "0", tn, _, _, _, _, _, _, p, "METHOD_PARAM", ref), + tn != "", java_method_parameter(_, _, m, _, _, _, _, _, _, _, _, _, p), + java_method_type_parameter(tn, _, _, _, _, _, _, m, _, _). +ref_names_tvar(ref) :- java_type_reference(_, _, _, _, _, _, _, "0", tn, _, _, _, _, _, _, l, "LOCAL_VARIABLE", ref), + tn != "", java_local_variable(_, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, l), + java_method_type_parameter(tn, _, _, _, _, _, _, m, _, _). candidate_ref(ref, tn, ctn, fp) :- java_type_reference(kind, ctx, _, _, _, _, _, "0", tn, ctn, _, _, _, _, _, _, _, ref), ext_ref_kind(kind), ext_ref_context(ctx), @@ -85,7 +102,8 @@ candidate_ref(ref, tn, ctn, fp) :- java_type_reference(kind, ctx, _, _, _, _, _, tn != "var", ref_file(ref, tn, fp), !name_resolves_in_file(tn, fp), - !simple_name_declared(tn). + !simple_name_declared(tn), + !ref_names_tvar(ref). // ── ref_external_name(Ref, QualifiedName) ─────────────────────────────────── ref_external_name(ref, q) :- candidate_ref(ref, tn, _, fp), @@ -133,7 +151,24 @@ local_creation_init(local) :- java_expression("ANONYMOUS_CLASS_CREATION", "ROOT" local_reassigned(local) :- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), java_expression(_, "ASSIGNMENT_TARGET", _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, e), expr_ultimate_method("client", e, m). -local_creation_pinned(local) :- local_creation_init(local), !local_reassigned(local). +local_creation_pinned(local) :- local_creation_init(local), !local_reassigned(local), !local_creation_external(local). +// ...but a creation of a type no staged IR declares pins nothing: `final StringBuilder sb = new StringBuilder()` +// with no JDK staged has no flow type at all, so `sb.toString()` was an unresolved site -- and every unresolved +// `toString` / `append` was a by-name caller of each client method of that name. The declared type is then the +// answer, exactly as for an initializer that is not a creation: an external receiver, a library boundary. +.decl creation_ref_external(ref:symbol) +creation_ref_external(ref) :- java_type_reference(kind, "OBJECT_CREATION_TYPE", _, _, _, _, _, "0", tn, _, _, _, _, _, _, _, _, ref), + ext_ref_kind(kind), + tn != "", + ref_file(ref, tn, fp), + !name_resolves_in_file(tn, fp), + !simple_name_declared(tn), + !ref_names_tvar(ref). +.decl local_creation_external(local:symbol) +local_creation_external(local) :- + java_expression("OBJECT_CREATION", "ROOT", "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, obj), + java_type_reference(_, "OBJECT_CREATION_TYPE", _, _, _, _, _, "0", _, _, _, _, _, _, _, obj, "EXPRESSION", ref), + creation_ref_external(ref). expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), expr_ultimate_type("client", e, encl), @@ -160,9 +195,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), @@ -213,6 +247,20 @@ expr_call_candidate(call, m) :- call_site(call, name, recv), external_target(call, label) :- call_site(call, name, recv), expr_type("external", recv, x), label = cat(x, cat(".", name)). +// (a') the receiver is a TYPE NAME no staged IR declares: a static call on the platform or a dependency, +// `Character.charCount(cp)`, `Objects.equals(a, b)`, `Collections.unmodifiableMap(m)`. A static call never +// dispatches, so no client method runs behind it. The parser writes the receiver as an identifier it read as a +// type (not ambiguous with a variable) with the qualified name its imports give; left unresolved, each one was a +// by-name caller of every client method called `equals`, `valueOf`, `max`, `get`. +.decl external_static_receiver(recv:symbol, q:symbol) +external_static_receiver(recv, q) :- + java_expression("IDENTIFIER_REFERENCE", "RECEIVER", _, _, _, _, _, _, _, _, n, _, _, _, "TYPE", "", _, q, "false", _, _, _, _, _, recv), + q != "", n != "", + !qualified_name_declared(q), + !simple_name_declared(n). +external_target(call, label) :- call_site(call, name, recv), + external_static_receiver(recv, q), + label = cat("external:", cat(q, cat(".", name))). // The parser writes parameterCount as text, so the written arity is compared as text. call_arity(call, pc) :- call_argc(call, n), pc = to_string(n). call_arity(call, "0") :- call_site(call, _, _), !call_has_args(call). diff --git a/graph/java/engine/resolution/generated-members.dl b/graph/java/engine/resolution/generated-members.dl index 4d9e9639..355592ea 100644 --- a/graph/java/engine/resolution/generated-members.dl +++ b/graph/java/engine/resolution/generated-members.dl @@ -13,12 +13,15 @@ // WHAT IT DOES NOT DO. It declares members, it does not give them bodies. A call // to a generated accessor is an edge to a member with no statements, which is the // truth: the accessor reads or writes a field and calls nothing further. Nothing -// here invents a call edge out of a generated member. +// here invents a call edge out of a generated member, and that includes a +// `@Delegate` forwarder: the call reaches the forwarder, not the delegate. // -// SCOPE: accessors, and the logger field a class-level annotation declares. -// `@Builder` needs a synthesized nested type as well as methods, and -// `@RequiredArgsConstructor` and friends need constructors; neither is here, and the -// case pins both as still unresolved so the gaps stay visible rather than silent. +// SCOPE: accessors (with the shapes `@Accessors` and lombok.config give them), +// `@With`, the logger field a class-level annotation declares and its type, +// constructors and their `staticName` factories, `@Builder` (a synthesized nested +// builder type with its setters, `build()`, `builder()` and `toBuilder()`), and +// `@Delegate` forwarders. `@SuperBuilder`, `@Singular` and a `@Builder` on a +// constructor or a static method are not modelled. // // THE LOGGER FIELD took two goes. Declaring it in `field_declared_in` did nothing, // because the site was never created to attach it to: field_access.dl:96 calls a @@ -35,11 +38,13 @@ // convention external-types.dl already uses for a type no IR declares. The bundle // synthesizes the row from the id, so the member is named in graph.sqlite and // carries provenance `generated`: a consumer can always tell a member read out of -// source from one inferred from an annotation. +// source from one inferred from an annotation. A synthesized builder TYPE is +// `generated:.`, and its members hang off that name. // // STRATIFICATION. `gen_method_live` and everything it is derived from read only -// PROJECTIONS OVER BASE RELATIONS (annotation_on, field_decl, field_modifier, -// type_decl), never the expr_type fixpoint. That is what lets it feed +// PROJECTIONS OVER BASE RELATIONS (annotation_on, ann_arg, field_decl, +// field_modifier, type_decl, and for `@Delegate` the declared field type and the +// type hierarchy), never the expr_type fixpoint. That is what lets it feed // `sig_declared_in`, which is read under the `!shadowed_in_type` negation in // method-lookup.dl and for that reason may only be built from base facts. The // `method_declared_in` clause DOES read `lookup_type`, which is seeded from @@ -47,7 +52,8 @@ // in method-lookup.dl do. // // COST. One pass over the annotations already staged, bounded by the number of -// annotated fields. It adds no join over the library method table. +// annotated fields. It adds no join over the library method table except for a +// `@Delegate` field, which reads the methods of that one field's type. // ============================================================================ // ───────────────────────────────────────────────────────────────────────────── @@ -61,10 +67,12 @@ // (a) on a FIELD: declares an accessor for that field alone. gen_field_getter_ann("Getter"). gen_field_setter_ann("Setter"). +gen_field_with_ann("With"). gen_field_with_ann("Wither"). // (b) on a TYPE: declares the accessor for every instance field of the type. gen_type_getter_ann("Getter"). gen_type_getter_ann("Data"). gen_type_setter_ann("Setter"). gen_type_setter_ann("Data"). +gen_type_with_ann("With"). gen_type_with_ann("Wither"). // `Value` is TWO different annotations sharing a simple name: the processor's, on // a TYPE, which declares a getter per field; and the framework's config injection, @@ -74,12 +82,30 @@ gen_type_setter_ann("Setter"). gen_type_setter_ann("Data"). // rather than one list consulted from both places. gen_type_getter_ann("Value"). -// (c) on a TYPE: declares a logger FIELD of the given name. +// (c) on a TYPE: declares a logger FIELD of the given name, of the given type. gen_logger_ann("Slf4j", "log"). gen_logger_ann("XSlf4j", "log"). gen_logger_ann("Log4j", "log"). gen_logger_ann("Log4j2", "log"). gen_logger_ann("CommonsLog", "log"). gen_logger_ann("JBossLog", "log"). gen_logger_ann("Flogger", "log"). gen_logger_ann("Log", "log"). +// The type is fixed by the annotation, so nothing is inferred (#1408). +gen_logger_type("Slf4j", "org.slf4j.Logger"). +gen_logger_type("XSlf4j", "org.slf4j.ext.XLogger"). +gen_logger_type("Log4j", "org.apache.log4j.Logger"). +gen_logger_type("Log4j2", "org.apache.logging.log4j.Logger"). +gen_logger_type("CommonsLog", "org.apache.commons.logging.Log"). +gen_logger_type("JBossLog", "org.jboss.logging.Logger"). +gen_logger_type("Flogger", "com.google.common.flogger.FluentLogger"). +gen_logger_type("Log", "java.util.logging.Logger"). + +// (d) the element that turns a constructor annotation into a static factory as well +// (`@AllArgsConstructor(staticName = "of")`, `@Data(staticConstructor = "of")`). +gen_static_arg("NoArgsConstructor", "staticName"). +gen_static_arg("AllArgsConstructor", "staticName"). +gen_static_arg("RequiredArgsConstructor", "staticName"). +gen_static_arg("Data", "staticConstructor"). +gen_static_arg("Value", "staticConstructor"). + // ───────────────────────────────────────────────────────────────────────────── // 1. WHICH FIELDS CARRY AN ACCESSOR @@ -91,46 +117,48 @@ gen_logger_ann("Flogger", "log"). gen_logger_ann("Log", "log"). // ───────────────────────────────────────────────────────────────────────────── gen_field_static(f) :- field_modifier(_, _, mod, f), contains("STATIC", mod). -// gen_accessor_demand(Prov, OwnerType, FieldName, FieldTypeName, Kind) -gen_accessor_demand(p, owner, fname, ftype, "get") :- +// gen_acc(Prov, OwnerType, FieldName, FieldTypeName, FieldHash, Kind): Kind is +// "get", "set" or "with". Every name, return type and field link below reads this +// one relation, so the field a member was built from is never lost on the way. +gen_acc(p, owner, fname, ftype, f, "get") :- annotation_on(p, ann, _, "FIELD_DECLARATION", f, _), gen_field_getter_ann(ann), field_decl(p, fname, ftype, owner, f). -gen_accessor_demand(p, owner, fname, ftype, "set") :- +gen_acc(p, owner, fname, ftype, f, "set") :- annotation_on(p, ann, _, "FIELD_DECLARATION", f, _), gen_field_setter_ann(ann), field_decl(p, fname, ftype, owner, f). -gen_accessor_demand(p, owner, fname, ftype, "get") :- +gen_acc(p, owner, fname, ftype, f, "with") :- + annotation_on(p, ann, _, "FIELD_DECLARATION", f, _), gen_field_with_ann(ann), + field_decl(p, fname, ftype, owner, f). +gen_acc(p, owner, fname, ftype, f, "get") :- annotation_on(p, ann, _, "TYPE_DECLARATION", owner, _), gen_type_getter_ann(ann), field_decl(p, fname, ftype, owner, f), !gen_field_static(f). -gen_accessor_demand(p, owner, fname, ftype, "set") :- +gen_acc(p, owner, fname, ftype, f, "set") :- annotation_on(p, ann, _, "TYPE_DECLARATION", owner, _), gen_type_setter_ann(ann), field_decl(p, fname, ftype, owner, f), !gen_field_static(f). +gen_acc(p, owner, fname, ftype, f, "with") :- + annotation_on(p, ann, _, "TYPE_DECLARATION", owner, _), gen_type_with_ann(ann), + field_decl(p, fname, ftype, owner, f), !gen_field_static(f). -// gen_accessor_src(Prov, OwnerType, FieldName, FieldHash, Kind) — the same four -// derivations as above, keeping the FIELD so section 7 can give the getter a return -// type. Carried separately rather than widening gen_accessor_demand, because that -// relation is joined on in four places and its shape is the reason the name rules -// below read cleanly. -gen_accessor_src(p, owner, fname, f, "get") :- - annotation_on(p, ann, _, "FIELD_DECLARATION", f, _), gen_field_getter_ann(ann), - field_decl(p, fname, _, owner, f). -gen_accessor_src(p, owner, fname, f, "get") :- - annotation_on(p, ann, _, "TYPE_DECLARATION", owner, _), gen_type_getter_ann(ann), - field_decl(p, fname, _, owner, f), !gen_field_static(f). +// gen_accessor_demand(Prov, OwnerType, FieldName, FieldTypeName, Kind): the +// projection the prefix rules below key on. +gen_accessor_demand(p, owner, fname, ftype, kind) :- gen_acc(p, owner, fname, ftype, _, kind). // ───────────────────────────────────────────────────────────────────────────── -// 1b. THE ACCESSOR PREFIX (lombok.config, #888) +// 1b. lombok.config (#888, #1406) // -// lombok.config decides the NAMES the processor generates, and reading only the field -// spelling gets them wrong wherever it is set. `lombok.accessors.prefix += m` on a -// field `mDepth` produces getDepth(), not getMDepth(). The wrong name is worse than no -// name, for the reason section 2 gives: it DECLARES a member the artefact does not -// have, and nothing downstream can tell it from a real one. +// lombok.config decides the NAMES and SHAPES the processor generates, and reading only +// the field spelling gets them wrong wherever it is set. `lombok.accessors.prefix += m` +// on a field `mDepth` produces getDepth(), not getMDepth(); `lombok.accessors.fluent = +// true` produces depth() and depth(v); `lombok.accessors.chain = true` makes every +// setter return `this`. The wrong name is worse than no name, for the reason section 2 +// gives: it DECLARES a member the artefact does not have, and nothing downstream can +// tell it from a real one. // // The file is in .properties format with no .properties extension, so the parser // matches it by name (FILE_EXTENSIONS.LOMBOK_CONFIG). // // SCOPE. A lombok.config applies to the directory tree below it. Rather than model -// that tree walk, the prefix is taken per PROVENANCE: one config per parsed root is +// that tree walk, a key is taken per PROVENANCE: one config per parsed root is // the shape every module in practice has, and a repository that sets a DIFFERENT // prefix in two subtrees of one module would need the walk. That case is declared, not // silently mis-named: gen_prefix_ambiguous drops both and section 2 falls back to the @@ -142,32 +170,36 @@ gen_accessor_src(p, owner, fname, f, "get") :- // Getting that wrong would rename every field that happens to start with the prefix // letter. // ───────────────────────────────────────────────────────────────────────────── -gen_prefix_key("lombok.accessors.prefix"). +gen_cfg_key("lombok.accessors.prefix"). +gen_cfg_key("lombok.accessors.chain"). +gen_cfg_key("lombok.accessors.fluent"). -// The value of the prefix key, from a lombok.config on either side. -gen_prefix_value(prov, v) :- gen_prefix_key(k), prop_key(prov, k, "true", f, h), +// The raw value of each key, from a lombok.config on either side. +gen_cfg_value(prov, k, v) :- gen_cfg_key(k), prop_key(prov, k, "true", f, h), contains("lombok.config", f), prop_segment(prov, h, v, _, _, _, _), v != "". // `+=` is the idiomatic spelling in this file and the properties scanner does not know -// it: the line splits on the first space, so the VALUE arrives as "+= m". Peeled here -// rather than taught to the scanner, because `+=` is a lombok.config-ism and a -// .properties file that happens to contain one must keep splitting the way it does -// today. Repeated single-character peeling, the same shape cfg_strip uses for quotes. -// A prefix is an identifier fragment, so it can never legitimately start with one of -// these three characters and nothing real is eaten. +// it: the line splits on the first space, so the VALUE arrives as "+= m" (and "= true" +// for a key written `key = true`). Peeled here rather than taught to the scanner, +// because `+=` is a lombok.config-ism and a .properties file that happens to contain one +// must keep splitting the way it does today. Repeated single-character peeling, the same +// shape cfg_strip uses for quotes. A value here is an identifier fragment or a boolean, +// so it can never legitimately start with one of these three characters and nothing +// real is eaten. gen_pfx_junk(s) :- gen_pfx_step(_, s), strlen(s) > 0, substr(s, 0, 1) = "+". gen_pfx_junk(s) :- gen_pfx_step(_, s), strlen(s) > 0, substr(s, 0, 1) = "=". gen_pfx_junk(s) :- gen_pfx_step(_, s), strlen(s) > 0, substr(s, 0, 1) = " ". -gen_pfx_step(v, v) :- gen_prefix_value(_, v). +gen_pfx_step(v, v) :- gen_cfg_value(_, _, v). gen_pfx_step(v, r) :- gen_pfx_step(v, s), gen_pfx_junk(s), strlen(s) > 1, r = substr(s, 1, strlen(s) - 1). -gen_prefix_raw(prov, c) :- gen_prefix_value(prov, v), gen_pfx_step(v, c), +gen_cfg_clean(prov, k, c) :- gen_cfg_value(prov, k, v), gen_pfx_step(v, c), !gen_pfx_junk(c), c != "". +gen_prefix_raw(prov, c) :- gen_cfg_clean(prov, "lombok.accessors.prefix", c). gen_prefix(prov, v) :- gen_prefix_raw(prov, v), !gen_prefix_ambiguous(prov). gen_prefix_ambiguous(prov) :- gen_prefix_raw(prov, a), gen_prefix_raw(prov, b), a != b. @@ -187,6 +219,36 @@ gen_effective_name(prov, fname, stripped) :- gen_prefix_applies(prov, fname, str gen_effective_name(prov, fname, fname) :- gen_accessor_demand(prov, _, fname, _, _), !gen_prefix_applies(prov, fname, _). +// ───────────────────────────────────────────────────────────────────────────── +// 1c. THE ACCESSOR SHAPE: @Accessors(fluent = true | chain = true) (#1406) +// +// `fluent = true` names the accessors after the field (`label()`, `label(v)`) instead of +// get/set, and implies `chain = true` unless chain is written false. `chain = true` makes +// a setter return the owner, so `s.setHost(h).setPort(1)` is one chain. The annotation on +// the FIELD decides; else the one on its TYPE; else the lombok.config key. Each option is +// decided on its own, which is how the processor merges them. +// ───────────────────────────────────────────────────────────────────────────── +gen_style_opt("chain"). +gen_style_opt("fluent"). + +gen_acc_ann_field(f, opt, v) :- annotation_on(_, "Accessors", _, "FIELD_DECLARATION", f, a), + ann_arg(_, a, opt, v, _, _), gen_style_opt(opt). +gen_acc_ann_type(owner, opt, v) :- annotation_on(_, "Accessors", _, "TYPE_DECLARATION", owner, a), + ann_arg(_, a, opt, v, _, _), gen_style_opt(opt). +gen_acc_field_has(f, opt) :- gen_acc_ann_field(f, opt, _). +gen_acc_type_has(owner, opt) :- gen_acc_ann_type(owner, opt, _). + +gen_style(f, opt, v) :- gen_acc(_, _, _, _, f, _), gen_acc_ann_field(f, opt, v). +gen_style(f, opt, v) :- gen_acc(_, owner, _, _, f, _), gen_acc_ann_type(owner, opt, v), + !gen_acc_field_has(f, opt). +gen_style(f, opt, v) :- gen_acc(p, owner, _, _, f, _), gen_style_opt(opt), + !gen_acc_field_has(f, opt), !gen_acc_type_has(owner, opt), + gen_cfg_clean(p, k, v), k = cat("lombok.accessors.", opt). + +gen_fluent(f) :- gen_style(f, "fluent", "true"). +gen_chain(f) :- gen_style(f, "chain", "true"). +gen_chain(f) :- gen_fluent(f), !gen_style(f, "chain", "false"). + // ───────────────────────────────────────────────────────────────────────────── // 2. THE NAME // @@ -196,42 +258,72 @@ gen_effective_name(prov, fname, fname) :- gen_accessor_demand(prov, _, fname, _, // declares a method the artefact does not have, and an invented member is worse // than a missing one, because nothing downstream can tell it from a real edge. // +// A primitive boolean ALREADY named `is` + an uppercase letter keeps its name for the +// getter and drops the `is` for the setter and the wither: `isLocked` -> `isLocked()`, +// `setLocked(v)`, `withLocked(v)` (#1404). `island` is not such a name, because `l` is +// lowercase: it is `isIsland()`, the ordinary rule. +// // cfg_upper is reused rather than re-tabled. Souffle sees one program: every .decl // is central in souffle/decls_all.dl and a rule may read a relation defined in any // phase folder, so the 26 facts stay in the one place that already holds them. // ───────────────────────────────────────────────────────────────────────────── -// Capitalize the EFFECTIVE name, which is the field name with any configured accessor -// prefix removed. gen_effective_name is the identity when no prefix is configured, so -// a project without a lombok.config derives exactly what it derived before. -gen_capitalized(n, c) :- gen_effective_name(_, _, n), strlen(n) > 0, +gen_is_stripped(p, f, s) :- gen_acc(p, _, fname, "boolean", f, _), + gen_effective_name(p, fname, eff), strlen(eff) > 2, + substr(eff, 0, 2) = "is", cfg_lower(substr(eff, 2, 1), _), + s = substr(eff, 2, strlen(eff) - 2). + +// gen_prop(Prov, FieldHash, PropertyName): the name the get/set/with prefix goes on. +gen_prop(p, f, s) :- gen_is_stripped(p, f, s). +gen_prop(p, f, eff) :- gen_acc(p, _, fname, _, f, _), + gen_effective_name(p, fname, eff), !gen_is_stripped(p, f, _). + +// Capitalize the property name, which is the field name with any configured accessor +// prefix (and a boolean's `is`) removed. +gen_cap_demand(n) :- gen_prop(_, _, n). +gen_capitalized(n, c) :- gen_cap_demand(n), strlen(n) > 0, cfg_upper(substr(n, 0, 1), up), c = cat(up, substr(n, 1, strlen(n) - 1)). -// ALREADY capitalized, which is what stripping an accessor prefix leaves behind: -// `mDepth` strips to `Depth`. cfg_upper maps lowercase to uppercase and has no key for -// an uppercase letter, so without this clause the stripped name capitalizes to nothing -// and the accessor is not declared at all, under either name. -gen_capitalized(n, n) :- gen_effective_name(_, _, n), strlen(n) > 0, +// ALREADY capitalized, which is what stripping a prefix leaves behind: `mDepth` strips +// to `Depth`. cfg_upper maps lowercase to uppercase and has no key for an uppercase +// letter, so without this clause the stripped name capitalizes to nothing and the +// accessor is not declared at all, under either name. +gen_capitalized(n, n) :- gen_cap_demand(n), strlen(n) > 0, cfg_lower(substr(n, 0, 1), _). -// gen_accessor(Prov, OwnerType, MethodName, Arity) +// gen_acc_named(Prov, OwnerType, FieldHash, Kind, MethodName, Arity) // Arity is a SYMBOL, not a number, because sig_declared_in carries parameterCount // as the symbol the IR wrote; a number here would not join with it. -gen_accessor(p, owner, cat("get", c), "0") :- - gen_accessor_demand(p, owner, fname, ftype, "get"), ftype != "boolean", - gen_effective_name(p, fname, eff), gen_capitalized(eff, c). -gen_accessor(p, owner, cat("is", c), "0") :- - gen_accessor_demand(p, owner, fname, "boolean", "get"), - gen_effective_name(p, fname, eff), gen_capitalized(eff, c). -gen_accessor(p, owner, cat("set", c), "1") :- - gen_accessor_demand(p, owner, fname, _, "set"), - gen_effective_name(p, fname, eff), gen_capitalized(eff, c). +gen_acc_named(p, owner, f, "get", cat("get", c), "0") :- gen_acc(p, owner, _, ftype, f, "get"), + !gen_fluent(f), ftype != "boolean", gen_prop(p, f, n), gen_capitalized(n, c). +gen_acc_named(p, owner, f, "get", cat("is", c), "0") :- gen_acc(p, owner, _, "boolean", f, "get"), + !gen_fluent(f), gen_prop(p, f, n), gen_capitalized(n, c). +gen_acc_named(p, owner, f, "set", cat("set", c), "1") :- gen_acc(p, owner, _, _, f, "set"), + !gen_fluent(f), gen_prop(p, f, n), gen_capitalized(n, c). +// fluent: the name is the field's, after the prefix is removed (a boolean keeps its `is`). +gen_acc_named(p, owner, f, "get", eff, "0") :- gen_acc(p, owner, fname, _, f, "get"), + gen_fluent(f), gen_effective_name(p, fname, eff). +gen_acc_named(p, owner, f, "set", eff, "1") :- gen_acc(p, owner, fname, _, f, "set"), + gen_fluent(f), gen_effective_name(p, fname, eff). +// @With is not affected by fluent: it is always `with` + the property name. +gen_acc_named(p, owner, f, "with", cat("with", c), "1") :- gen_acc(p, owner, _, _, f, "with"), + gen_prop(p, f, n), gen_capitalized(n, c). + +// gen_accessor(Prov, OwnerType, MethodName, Arity) +gen_accessor(p, owner, name, pc) :- gen_acc_named(p, owner, _, _, name, pc). // ───────────────────────────────────────────────────────────────────────────── // 3. THE MEMBERS, AND THEIR IDS +// +// gen_member(Prov, OwnerType, OwnerQualifiedName, Name, Arity) is the ONE list every +// kind of generated method joins: the accessors here, and the factories, builder members +// and forwarders of sections 6-9. The owner's qualified name is carried because a +// synthesized builder type is in no type_decl row to read it from. // ───────────────────────────────────────────────────────────────────────────── +gen_member(p, owner, q, name, pc) :- gen_accessor(p, owner, name, pc), + type_decl(p, _, q, _, _, _, owner). + // gen_method(OwnerType, Name, Arity, MethodId) -gen_method(owner, name, pc, id) :- gen_accessor(p, owner, name, pc), - type_decl(p, _, qname, _, _, _, owner), - id = cat("generated:", cat(qname, cat("#", cat(name, cat("/", pc))))). +gen_method(owner, name, pc, id) :- gen_member(_, owner, q, name, pc), + id = cat("generated:", cat(q, cat("#", cat(name, cat("/", pc))))). // ───────────────────────────────────────────────────────────────────────────── @@ -271,18 +363,18 @@ sig_concrete_in(t, name, pc) :- gen_method_live(t, name, pc, _). // method_owner, and projections/methods.dl builds that from java_method / // lib_method alone. Without this clause a generated member reached expr_pruned and // then produced nothing: it owns no row there. The provenance carried through from -// gen_accessor is what decides `known_edge` against `boundary_lib`, so a generated +// gen_member is what decides `known_edge` against `boundary_lib`, so a generated // member on a library type is a boundary edge exactly as a hand-written one is. method_owner(p, owner, id) :- gen_method_live(owner, name, pc, id), - gen_accessor(p, owner, name, pc). + gen_member(p, owner, _, name, pc). // ARITY. overload.dl counts parameters from java_method_parameter / // lib_method_parameter, so a generated method has no count and only the zero-arg // clause of arity_match can fire: every getter resolved and every SETTER was -// dropped at the arity gate. A setter takes exactly one argument, which the -// annotation determines without any parameter row, so state it directly. The -// getters need no clause: zero args against zero params is already a match. -method_paramc(m, 1) :- gen_method_live(_, _, "1", m). +// dropped at the arity gate. The annotation determines the count without any +// parameter row, so state it directly. Zero args against zero params is already a +// match, which is why the zero-arity members need no row. +method_paramc(m, n) :- gen_method_live(_, _, pc, m), pc != "0", n = to_number(pc). // gen_field(OwnerType, Name, FieldId) gen_field(owner, fname, id) :- @@ -313,6 +405,34 @@ recv_name_is_field(e) :- ident_field_recv(e, name), expr_ultimate_type("client", field_prov(p, id) :- gen_field_live(owner, fname, id), annotation_on(p, ann, _, "TYPE_DECLARATION", owner, _), gen_logger_ann(ann, fname). +// ── 4b. THE LOGGER FIELD'S TYPE (#1408) ──────────────────────────────────── +// The field was declared by name only, so `log.info(..)` had a receiver of no type and +// the call was unresolved, while the same call through a hand-declared Logger field is +// a library boundary. The annotation fixes the type: a staged library type when there +// is one, a client type of that name, and otherwise the external node external-types.dl +// mints for any type the client names and no IR declares. +gen_field_type_q(id, q) :- gen_field_live(owner, fname, id), + annotation_on(_, ann, _, "TYPE_DECLARATION", owner, _), gen_logger_ann(ann, fname), + gen_logger_type(ann, q). +gen_field_type(id, "lib", t) :- gen_field_type_q(id, q), + lib_type(_, q, _, _, _, _, _, _, _, _, _, _, _, t). +gen_field_type(id, "client", t) :- gen_field_type_q(id, q), + java_type(_, q, _, _, _, _, _, _, _, _, _, _, _, t). +gen_field_type(id, "external", t) :- gen_field_type_q(id, q), + !qualified_name_declared(q), t = cat("external:", q). +external_type(t) :- gen_field_type(_, "external", t). + +// The receiver: a bare name the parser tagged FIELD, in the declaring type, a subtype of +// it, or a type nested in it (a logger used from an inner or anonymous class). +gen_field_recv(e, id) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), + expr_ultimate_type("client", e, encl), gen_field_live(encl, name, id). +gen_field_recv(e, id) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), + expr_ultimate_type("client", e, encl), type_ancestor(encl, anc), gen_field_live(anc, name, id). +gen_field_recv(e, id) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), + expr_ultimate_type("client", e, encl), type_in_type("client", encl, outer), + gen_field_live(outer, name, id). +expr_type(prov, e, t) :- gen_field_recv(e, id), gen_field_type(id, prov, t). + // ───────────────────────────────────────────────────────────────────────────── // 5. OVERRIDE, which is a sixth relation built off the IR tables // @@ -462,6 +582,22 @@ ctor_of("client", owner, id) :- gen_ctor_live(owner, _, id). method_paramc(id, n) :- gen_ctor_live(_, n, id), n > 0. method_owner("client", owner, id) :- gen_ctor_live(owner, _, id). +// ── 6b. THE STATIC FACTORY (#1407) ───────────────────────────────────────── +// `@AllArgsConstructor(staticName = "of")` makes the constructor private and declares +// `static Point of(int x, int y)` beside it, with the constructor's arity. `Point.of(1, +// 2)` names no constructor, so without this the call and everything chained onto it +// were unresolved. +gen_ctor_static(p, owner, shape, v) :- annotation_on(p, ann, _, "TYPE_DECLARATION", owner, a), + gen_ctor_ann(ann, shape), gen_static_arg(ann, arg), ann_arg(_, a, arg, v, _, _), v != "". +gen_ctor_arity(owner, "none", 0) :- gen_ctor_demand(_, owner, "none"). +gen_ctor_arity(owner, "all", n) :- gen_ctor_all_count(owner, n). +gen_ctor_arity(owner, "required", n) :- gen_ctor_req_count(owner, n). + +gen_member(p, owner, q, v, to_string(n)) :- gen_ctor_static(p, owner, shape, v), + gen_ctor_arity(owner, shape, n), type_decl(p, _, q, _, _, _, owner). +gen_ret_self(id, owner) :- gen_ctor_static(_, owner, shape, v), + gen_ctor_arity(owner, shape, n), gen_method_live(owner, v, to_string(n), id). + // ───────────────────────────────────────────────────────────────────────────── // 7. THE RETURN TYPE (#885) @@ -477,9 +613,10 @@ method_owner("client", owner, id) :- gen_ctor_live(owner, _, id). // next door as field_type_resolves, so this is a join, not new machinery: the getter // returns exactly what the field holds. // -// Getters only. A setter returns void, and @Accessors(chain = true) making it return -// `this` is a different annotation with a different answer, deliberately not guessed -// here. +// A member that returns its OWNER (a chain setter, a wither, a static factory, every +// builder member) has no reference to reuse either, so it gets a synthetic one, +// `generated-ref:`. It names no IR node, so every consumer that walks a reference's +// type arguments finds none, which is the truth: the owner is written without any. // // gen_accessor_field(MethodId, FieldHash) — which field a generated getter reads. // @@ -491,25 +628,138 @@ method_owner("client", owner, id) :- gen_ctor_live(owner, _, id). // // Getters only. A setter WRITES the field, and cfg_field_read is a read relation; // modelling the write is a separate question with a separate consumer. -gen_accessor_field(id, f) :- gen_accessor_src(_, owner, fname, f, "get"), - gen_capitalized(fname, c), - gen_method_live(owner, cat("get", c), "0", id). -gen_accessor_field(id, f) :- gen_accessor_src(_, owner, fname, f, "get"), - gen_capitalized(fname, c), - gen_method_live(owner, cat("is", c), "0", id). - -// The REF column is the field's own type reference. Reusing it rather than minting a -// synthetic one keeps every downstream consumer that reads the reference (type -// arguments, generic binding) pointing at a node the IR actually has. // ───────────────────────────────────────────────────────────────────────────── -gen_return_type(id, ref, t) :- gen_accessor_src(p, owner, fname, f, "get"), - gen_capitalized(fname, c), - gen_method_live(owner, cat("get", c), "0", id), - field_type_resolves(p, f, ref, t). -gen_return_type(id, ref, t) :- gen_accessor_src(p, owner, fname, f, "get"), - gen_capitalized(fname, c), - gen_method_live(owner, cat("is", c), "0", id), - field_type_resolves(p, f, ref, t). - -method_return_type_resolves(p, id, ref, t) :- gen_return_type(id, ref, t), - gen_method_live(owner, _, _, id), type_decl(p, _, _, _, _, _, owner). +gen_accessor_field(id, f) :- gen_acc_named(_, owner, f, "get", name, "0"), + gen_method_live(owner, name, "0", id). + +// gen_return_type(MethodId, Ref, Type): the getter returns the field's own type, +// through the field's own reference. +gen_return_type(id, ref, t) :- gen_accessor_field(id, f), + field_type_resolves(_, f, ref, t). + +// A setter returns the owner when it chains (#1406), and a wither always does (#1407). +gen_ret_self(id, owner) :- gen_acc_named(_, owner, f, "set", name, "1"), gen_chain(f), + gen_method_live(owner, name, "1", id). +gen_ret_self(id, owner) :- gen_acc_named(_, owner, _, "with", name, "1"), + gen_method_live(owner, name, "1", id). +gen_return_type(id, ref, t) :- gen_ret_self(id, t), ref = cat("generated-ref:", id). + +// The provenance is the RESOLVED TYPE's, as for a record accessor (reference-types.dl): +// a client type returning a library type is the ordinary case. +method_return_type_resolves("client", id, ref, t) :- gen_return_type(id, ref, t), + type_prov(t, "client"). +method_return_type_resolves("lib", id, ref, t) :- gen_return_type(id, ref, t), + type_prov(t, "lib"). +method_return_type_resolves("client", id, ref, t) :- gen_return_type(id, ref, t), + gen_builder_type(_, _, t, _). + + +// ───────────────────────────────────────────────────────────────────────────── +// 8. @Builder (#1405) +// +// `@Builder` declares a nested `Builder` with one setter per field (named after +// the field, returning the builder), `build()` returning the owner, a static `builder()` +// on the owner, and, with `toBuilder = true`, an instance `toBuilder()`. None of it is in +// the source, so every call in `Order.builder().sku("a").build().ship()` was unresolved, +// including the hand-written `ship()` at its end. +// +// THE BUILDER TYPE. When the source writes a partial builder (`public static class +// OrderBuilder { ... }` inside Order, which the processor then fills), that type is the +// builder and its hand-written members win as they do anywhere else. Otherwise the type +// is synthesized: `generated:.`, a lookup type with no file, +// whose only members are the ones this section declares. +// +// builderClassName, builderMethodName and buildMethodName are read; toBuilder must be +// written `true`. A final field with an initialiser has no setter, as in the processor. +// ───────────────────────────────────────────────────────────────────────────── +gen_builder(p, owner, a) :- annotation_on(p, "Builder", _, "TYPE_DECLARATION", owner, a). + +gen_builder_opt(owner, opt, v) :- gen_builder(_, owner, a), ann_arg(_, a, opt, v, _, _), v != "". +gen_builder_opt_has(owner, opt) :- gen_builder_opt(owner, opt, _). + +// gen_builder_name(Owner, Which, Name) +gen_builder_name(owner, "class", v) :- gen_builder_opt(owner, "builderClassName", v). +gen_builder_name(owner, "class", cat(tn, "Builder")) :- gen_builder(p, owner, _), + !gen_builder_opt_has(owner, "builderClassName"), type_decl(p, tn, _, _, _, _, owner). +gen_builder_name(owner, "method", v) :- gen_builder_opt(owner, "builderMethodName", v). +gen_builder_name(owner, "method", "builder") :- gen_builder(_, owner, _), + !gen_builder_opt_has(owner, "builderMethodName"). +gen_builder_name(owner, "build", v) :- gen_builder_opt(owner, "buildMethodName", v). +gen_builder_name(owner, "build", "build") :- gen_builder(_, owner, _), + !gen_builder_opt_has(owner, "buildMethodName"). + +// gen_builder_type(Prov, Owner, BuilderType, BuilderQualifiedName) +gen_builder_written(owner, bt, bq) :- gen_builder_name(owner, "class", bn), + type_direct_parent("client", bt, owner), + java_type(bn, bq, _, _, _, _, _, _, _, _, _, _, _, bt). +gen_builder_has_written(owner) :- gen_builder_written(owner, _, _). +gen_builder_type(p, owner, bt, bq) :- gen_builder(p, owner, _), gen_builder_written(owner, bt, bq). +gen_builder_type(p, owner, bt, bq) :- gen_builder(p, owner, _), !gen_builder_has_written(owner), + gen_builder_name(owner, "class", bn), type_decl(p, _, q, _, _, _, owner), + bq = cat(q, cat(".", bn)), bt = cat("generated:", bq). + +lookup_type(bt) :- gen_builder_type(_, _, bt, _). + +// The fields with a setter: every instance field but a final one already initialised. +gen_builder_field_skip(f) :- field_modifier(_, _, mod, f), contains("FINAL", mod), + gen_field_initialized(f). +gen_builder_field(p, owner, fname) :- gen_builder(p, owner, _), + field_decl(p, fname, _, owner, f), !gen_field_static(f), !gen_builder_field_skip(f). + +// On the owner: builder() and toBuilder(), both returning the builder. +gen_member(p, owner, q, bm, "0") :- gen_builder_type(p, owner, _, _), + gen_builder_name(owner, "method", bm), type_decl(p, _, q, _, _, _, owner). +gen_member(p, owner, q, "toBuilder", "0") :- gen_builder_type(p, owner, _, _), + gen_builder_opt(owner, "toBuilder", "true"), type_decl(p, _, q, _, _, _, owner). +gen_ret_self(id, bt) :- gen_builder_type(_, owner, bt, _), gen_builder_name(owner, "method", bm), + gen_method_live(owner, bm, "0", id). +gen_ret_self(id, bt) :- gen_builder_type(_, owner, bt, _), gen_builder_opt(owner, "toBuilder", "true"), + gen_method_live(owner, "toBuilder", "0", id). + +// On the builder: a setter per field returning the builder, and build() returning the owner. +gen_member(p, bt, bq, fname, "1") :- gen_builder_type(p, owner, bt, bq), + gen_builder_field(p, owner, fname). +gen_member(p, bt, bq, b, "0") :- gen_builder_type(p, owner, bt, bq), + gen_builder_name(owner, "build", b). +gen_ret_self(id, bt) :- gen_builder_type(_, owner, bt, _), gen_builder_field(_, owner, fname), + gen_method_live(bt, fname, "1", id). +gen_ret_self(id, owner) :- gen_builder_type(_, owner, bt, _), gen_builder_name(owner, "build", b), + gen_method_live(bt, b, "0", id). + + +// ───────────────────────────────────────────────────────────────────────────── +// 9. @Delegate (#1407) +// +// `@Delegate private final Store inner` declares, on the owner, one forwarding method +// for each public instance method of the field's type. `new Cache().put("a")` names a +// method Cache does not write, so it was unresolved. The forwarder takes the delegated +// method's arity and return type. `types =` and `excludes =` are not read. +// +// BASE FACTS ONLY, and that decides the scope. A forwarder feeds sig_declared_in like any +// generated member, so it may not read the type-resolution fixpoint: the field's declared +// type is resolved by name in the owner's file (type_resolves_in_file, which reads imports +// and declarations), not through field_type_resolves, and only the methods the type +// DECLARES are forwarded, not the ones it inherits (type_ancestor is in that fixpoint too). +// A first draft read both and the program did not stratify. +// ───────────────────────────────────────────────────────────────────────────── +gen_delegate(p, owner, ft) :- annotation_on(p, "Delegate", _, "FIELD_DECLARATION", f, _), + field_decl(p, _, tn, owner, f), type_decl(p, _, _, fp, _, _, owner), + type_resolves_in_file(_, tn, fp, ft). + +gen_delegate_kind("INSTANCE_METHOD"). +gen_delegate_kind("ABSTRACT_METHOD"). + +// gen_delegate_target(FieldType, Name, Arity, DelegatedMethod) +gen_delegate_target(ft, name, pc, m) :- gen_delegate(_, _, ft), gen_delegate_kind(k), + java_method(name, _, _, _, _, _, _, ft, _, _, _, _, _, _, _, _, k, pc, _, _, _, m), name != "". +gen_delegate_target(ft, name, pc, m) :- gen_delegate(_, _, ft), gen_delegate_kind(k), + lib_method(name, _, _, _, _, _, _, ft, _, _, _, _, _, _, _, _, k, pc, _, _, _, m), name != "". + +gen_member(p, owner, q, name, pc) :- gen_delegate(p, owner, ft), + gen_delegate_target(ft, name, pc, _), type_decl(p, _, q, _, _, _, owner). + +gen_return_type(id, ref, t) :- gen_delegate(_, owner, ft), gen_delegate_target(ft, name, pc, m), + gen_method_live(owner, name, pc, id), method_return_type_resolves(_, m, ref, t). +gen_return_type(id, ref, t) :- gen_delegate(_, owner, ft), gen_delegate_target(ft, name, pc, m), + gen_method_live(owner, name, pc, id), lib_method_return_type(m, t), + ref = cat("generated-ref:", id). diff --git a/graph/java/engine/resolution/generic-chain.dl b/graph/java/engine/resolution/generic-chain.dl index 4c2ccdf3..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`. @@ -129,10 +126,134 @@ type_fixes_super(sub, super, superParam, argType) :- java_type_reference(k, _, _, _, _, sref, pos, "1", _, _, _, _, _, _, _, _, _, argRef), k != "TYPE_VARIABLE", type_ref_resolves(_, argRef, argType). +// class_tvar_binding(Type, Super, SuperParam, Arg): as seen from Type, Super's parameter is +// Arg. Seeded by the clause that fixes it, then carried two ways (#1478, #1412): +// UP, through a supertype that passes its own variable straight on. `ChainStub extends +// Mid`, `Mid extends Base`: Mid.S = ChainStub, so Base.S = ChainStub. +// Binding only the direct parent lost `withTimeout`'s S, declared on Base, and the call +// after it; one level (`DirectStub extends Base`) resolved. +// DOWN, to every subtype of the type that fixed it: a subclass of OrderService inherits +// Base.M = OrderMapper exactly as OrderService does. +// Bounded by the hierarchy (a finite DAG), and seeded only where a clause writes a concrete +// argument, so a type that passes a variable through adds nothing by itself. +class_tvar_binding(sub, super, superParam, argType) :- type_fixes_super(sub, super, superParam, argType). +class_tvar_binding(sub, super2, p2, argType) :- class_tvar_binding(sub, super1, p1, argType), + super_tvar_rename(super1, super2, p2, p1). +class_tvar_binding(sub2, super, p, argType) :- class_tvar_binding(sub, super, p, argType), + type_parent(sub2, sub), sub2 != sub. + // a receiver whose type fixes a supertype parameter carries that binding. expr_arg_binding(recv, super, superParam, argType) :- expr_type(_, recv, subType), - type_fixes_super(subType, super, superParam, argType). + class_tvar_binding(subType, super, superParam, argType). + +// ── THE RECEIVER IS IMPLICIT: an inherited member typed as the superclass's variable (#1412) ── +// `class OrderService extends Base` calling `getMapper().findClosed()` or +// `mapper.findOpen()`: the member is Base's, typed M, and the receiver is `this`, which no +// clause above sees because nothing is written at the call. The binding is the enclosing +// type's own, from its extends clause. The field form was worse than unresolved: M, read as +// a class name nothing declares, became an external type and the call a library boundary on +// `external:M` (external-types.dl now refuses a type variable's name). +expr_type(prov, call, argType) :- call_unqualified(call, _), + applicable_candidate(call, callee), + java_method_ret_tvar(callee, owner, tv), + expr_ultimate_type("client", call, encl), + class_tvar_binding(encl, owner, tv, argType), + type_prov(argType, prov). +expr_type(prov, call, argType) :- call_unqualified(call, _), + applicable_candidate(call, callee), + lib_method_ret_tvar(callee, owner, tv), + expr_ultimate_type("client", call, encl), + class_tvar_binding(encl, owner, tv, argType), + type_prov(argType, prov). + +// field_tvar(Field, OwnerType, TypeVariable): a field declared as its owner's type +// variable. The parser writes a field's variable as a CLASS reference carrying the name +// (the TYPE_VARIABLE kind appears in method signatures), so both encodings are read. +// java_type_reference (18): 0 kind 1 context 2 typeRegistryLinkHash 7 depth 8 typeName +// 10 typeVariableName 15 ownerHash 16 ownerKind +field_tvar(field, owner, tv) :- java_field(_, _, _, _, _, _, _, _, owner, _, _, _, _, field), + java_type_reference("CLASS", "FIELD_TYPE", _, _, _, _, _, "0", tv, _, _, _, _, _, _, field, "FIELD", _), + java_type_parameter(tv, _, _, _, _, _, owner, _). +field_tvar(field, owner, tv) :- java_field(_, _, _, _, _, _, _, _, owner, _, _, _, _, field), + java_type_reference("TYPE_VARIABLE", "FIELD_TYPE", _, _, _, _, _, "0", _, _, tv, _, _, _, _, field, "FIELD", _), + java_type_parameter(tv, _, _, _, _, _, owner, _). +field_tvar(field, owner, tv) :- lib_field(_, _, _, _, _, _, _, _, owner, _, _, _, _, field), + lib_type_reference("CLASS", "FIELD_TYPE", _, _, _, _, _, "0", tv, _, _, _, _, _, _, field, "FIELD", _), + lib_type_parameter(tv, _, _, _, _, _, owner, _). +field_tvar(field, owner, tv) :- lib_field(_, _, _, _, _, _, _, _, owner, _, _, _, _, field), + lib_type_reference("TYPE_VARIABLE", "FIELD_TYPE", _, _, _, _, _, "0", _, _, tv, _, _, _, _, field, "FIELD", _), + lib_type_parameter(tv, _, _, _, _, _, owner, _). + +// a bare inherited field: the enclosing type's binding for its declaring type's variable. +expr_type(prov, e, argType) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), + expr_ultimate_type("client", e, encl), + type_ancestor(encl, anc), + java_field(name, _, _, _, _, _, _, _, anc, _, _, _, _, field), + field_tvar(field, anc, tv), + class_tvar_binding(encl, anc, tv, argType), + type_prov(argType, prov). +expr_type(prov, e, argType) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), + expr_ultimate_type("client", e, encl), + type_ancestor(encl, anc), + lib_field(name, _, _, _, _, _, _, _, anc, _, _, _, _, field), + field_tvar(field, anc, tv), + class_tvar_binding(encl, anc, tv, argType), + type_prov(argType, prov). +// `this.mapper` / `svc.mapper`: the same, keyed on the qualifier's type. +expr_type(prov, e, argType) :- java_expression("FIELD_ACCESS", _, _, _, _, _, _, _, _, _, name, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + java_expression(_, "QUALIFIER", _, _, _, _, e, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, q), + expr_type(_, q, qtype), + type_ancestor(qtype, anc), + java_field(name, _, _, _, _, _, _, _, anc, _, _, _, _, field), + field_tvar(field, anc, tv), + class_tvar_binding(qtype, anc, tv, argType), + type_prov(argType, prov). +expr_type(prov, e, argType) :- java_expression("FIELD_ACCESS", _, _, _, _, _, _, _, _, _, name, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + java_expression(_, "QUALIFIER", _, _, _, _, e, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, q), + expr_type(_, q, qtype), + type_ancestor(qtype, anc), + lib_field(name, _, _, _, _, _, _, _, anc, _, _, _, _, field), + field_tvar(field, anc, tv), + class_tvar_binding(qtype, anc, tv, argType), + type_prov(argType, prov). + +// ── A GENERIC METHOD'S OWN VARIABLE, BOUND BY A CLASS LITERAL (#1547) ─────────────────────── +// `static T get(Class type)`, called `Beans.get(OrderStore.class).save("a")`: T is the +// METHOD's type parameter, and the only place it is fixed is the argument. Every clause above +// binds a CLASS's parameters from a receiver, so the call had no type and `save` was +// unresolved: the service-locator lookup (`getBean(X.class)`, `getMapper(X.class)`). The +// binding is sound where it applies: the argument IS `Class`, so T is X. +// Only a class literal written at the call binds; a Class-typed variable does not. +mtvar_ret(m, tv) :- java_type_reference("TYPE_VARIABLE", "METHOD_RETURN", _, _, _, _, _, "0", _, _, tv, _, _, _, _, m, "METHOD", _), + tv != "", java_method_type_parameter(tv, _, _, _, _, _, _, m, _, _). +mtvar_ret(m, tv) :- lib_type_reference("TYPE_VARIABLE", "METHOD_RETURN", _, _, _, _, _, "0", _, _, tv, _, _, _, _, m, "METHOD", _), + tv != "", lib_method_type_parameter(tv, _, _, _, _, _, _, m, _, _). + +// mtvar_class_param(Method, TypeVariable, Position): parameter Position is `Class`. +mtvar_class_param(m, tv, pos) :- java_method_parameter(_, pos, m, _, _, _, _, _, _, _, _, _, param), + java_type_reference(_, "METHOD_PARAM", _, _, _, _, _, "0", "Class", _, _, _, _, _, _, param, "METHOD_PARAM", cref), + java_type_reference("TYPE_VARIABLE", _, _, _, _, cref, "0", "1", _, _, tv, _, _, _, _, _, _, _). +mtvar_class_param(m, tv, pos) :- lib_method_parameter(_, pos, m, _, _, _, _, _, _, _, _, _, param), + lib_type_reference(_, "METHOD_PARAM", _, _, _, _, _, "0", "Class", _, _, _, _, _, _, param, "METHOD_PARAM", cref), + lib_type_reference("TYPE_VARIABLE", _, _, _, _, cref, "0", "1", _, _, tv, _, _, _, _, _, _, _). + +// class_literal_type(Expr, Prov, Type): the type a class literal names. +class_literal_type(e, "client", t) :- java_expression("CLASS_LITERAL", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, pqn, _, _, _, _, _, _, e), + pqn != "", java_type(_, pqn, _, _, _, _, _, _, _, _, _, _, _, t). +class_literal_type(e, "lib", t) :- java_expression("CLASS_LITERAL", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, pqn, _, _, _, _, _, _, e), + pqn != "", lib_type(_, pqn, _, _, _, _, _, _, _, _, _, _, _, t). +class_literal_type(e, prov, t) :- java_expression("CLASS_LITERAL", _, _, _, _, _, _, _, _, _, name, _, _, _, _, _, _, pqn, _, _, _, _, _, _, e), + name != "", !type_qname_known(pqn), + expr_ultimate_type("client", e, encl), + java_type(_, _, _, _, _, _, _, file, _, _, _, _, _, encl), + type_resolves_in_file(prov, name, file, t). + +expr_type(prov, call, t) :- java_expression("CLASS_LITERAL", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, lit), + applicable_candidate(call, callee), + mtvar_ret(callee, tv), + mtvar_class_param(callee, tv, pos), + class_literal_type(lit, prov, t). // ── NESTED generics (c4): `List> l; l.get(0).get()`. type_arg_binding only binds // TOP-LEVEL (depth-1) args, so the inner Box.T=Dog is lost. arg_ref_at keys on the PARENT 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/engine/resolution/type-arg-binding.dl b/graph/java/engine/resolution/type-arg-binding.dl index da9f3a93..e9353f1d 100644 --- a/graph/java/engine/resolution/type-arg-binding.dl +++ b/graph/java/engine/resolution/type-arg-binding.dl @@ -93,6 +93,17 @@ super_tvar_rename(sub, super, superParam, subVar) :- type_super_ref_h(sub, super lib_type_parameter(superParam, pos, _, _, _, _, super, _), java_type_reference("TYPE_VARIABLE", _, _, _, _, sref, pos, "1", _, _, subVar, _, _, _, _, _, _, _). +// Positional rename, client subtype -> CLIENT supertype (`class Mid extends Base`, both +// in the source). The clauses above only reach a library supertype, because the substitution +// downstream once read only library methods; java_method_ret_tvar and the implicit-receiver +// clauses in generic-chain.dl read client ones too, and a binding that stopped one level up a +// client hierarchy lost every member declared higher (#1478). +super_tvar_rename(sub, super, superParam, subVar) :- type_super_ref_h(sub, superName, sref), + type_parent(sub, super), + java_type(superName, _, _, _, _, _, _, _, _, _, _, _, _, super), + java_type_parameter(superParam, pos, _, _, _, _, super, _), + java_type_reference("TYPE_VARIABLE", _, _, _, _, sref, pos, "1", _, _, subVar, _, _, _, _, _, _, _). + // Transitive climb: a binding on Sub carries to each Super via the positional rename, // closing over the whole ancestor chain (bounded — the type hierarchy is a finite DAG). type_arg_binding(ref, super, superParam, argType) :- type_arg_binding(ref, sub, subVar, argType), diff --git a/graph/java/souffle/decls_all.dl b/graph/java/souffle/decls_all.dl index 625f6581..605f86b2 100644 --- a/graph/java/souffle/decls_all.dl +++ b/graph/java/souffle/decls_all.dl @@ -35,6 +35,7 @@ .decl entry_reachable(c0:symbol) .decl http_entry_ann(c0:symbol) .decl junit_entry_ann(c0:symbol) +.decl annotation_type_category(c0:symbol) .decl cast_type_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl client_calls_client(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl client_calls_lib(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -463,6 +464,7 @@ .decl lib_dispatch_width_low(c0:symbol,c1:number) .decl lib_wide_dispatch(c0:symbol) .decl dispatch_cap(c0:symbol) +.decl dispatch_closed_world(c0:symbol) .decl field_final(c0:symbol) .decl field_init_new_type(c0:symbol,c1:symbol) .decl field_multi_new(c0:symbol) @@ -473,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) @@ -488,6 +492,12 @@ .decl concrete_class_type(c0:symbol) .decl lib_method_ret_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_arg_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl class_tvar_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol) // (type, super, superParam, arg) as seen from type +.decl field_tvar(c0:symbol,c1:symbol,c2:symbol) // (field, ownerType, typeVariable) a field typed as its owner's variable +.decl mtvar_ret(c0:symbol,c1:symbol) // (method, its own type variable it returns) +.decl mtvar_class_param(c0:symbol,c1:symbol,c2:symbol) // (method, typeVariable, position of its Class parameter) +.decl class_literal_type(c0:symbol,c1:symbol,c2:symbol) // (classLiteralExpr, prov, type it names) +.decl ref_names_tvar(c0:symbol) // a reference whose name is a type variable in scope .decl type_fixes_super(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl arg_ref_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol) @@ -753,6 +763,76 @@ .decl grpc_impl_holder(c0:symbol,c1:symbol) .decl grpc_serves(c0:symbol,c1:symbol) .decl grpc_sends_at(c0:symbol,c1:symbol,c2:symbol) +// gRPC with the generated holder outside the source tree (#1548): the same pairing from written names +.decl grpc_generated_suffix(c0:symbol) +.decl grpc_ext_name_demand(c0:symbol,c1:symbol) +.decl grpc_ext_dot(c0:symbol,c1:number) +.decl grpc_ext_last_dot(c0:symbol,c1:number) +.decl grpc_ext_prev_dot(c0:symbol,c1:number) +.decl grpc_ext_has_prev(c0:symbol) +.decl grpc_ext_parts(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl grpc_ext_service(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_ext_server(c0:symbol) +.decl grpc_ext_client(c0:symbol) +.decl grpc_ext_impl_site(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_ext_send_site(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl grpc_ext_site(c0:symbol,c1:symbol) +.decl grpc_file_on_demand(c0:symbol) +.decl grpc_file_package(c0:symbol,c1:symbol) +.decl grpc_ext_holder_q(c0:symbol,c1:symbol,c2:symbol) +.decl grpc_rpc_shaped(c0:symbol) +.decl grpc_ext_handler(c0:symbol,c1:symbol,c2:symbol) +// framework entry points and HTTP destinations (#1400 #1410 #1428-#1430 #1456-#1458 #1463 #1464) +.decl cfg_jpa_callback_ann(c0:symbol) +.decl jpa_callback_owner(c0:symbol) +.decl cfg_jaxrs_param_ann(c0:symbol) +.decl cfg_jaxrs_param_factory(c0:symbol) +.decl cfg_servlet_registrar_type(c0:symbol) +.decl cfg_xml_servlet_name_elem(c0:symbol) +.decl cfg_xml_servlet_mapping_elem(c0:symbol) +.decl cfg_xml_url_pattern_elem(c0:symbol) +.decl cfg_servlet_url_arg(c0:symbol) +.decl cfg_jaxrs_client_owner(c0:symbol) +.decl cfg_jaxrs_builder_step(c0:symbol) +.decl cfg_subresource_locator_ann(c0:symbol) +.decl servlet_registrar_recv(c0:symbol) +.decl jaxrs_param_type(c0:symbol) +.decl string_param_0(c0:symbol) +.decl jaxrs_string_type(c0:symbol) +.decl path_ends_slash(c0:symbol) +.decl path_starts_slash(c0:symbol) +.decl path_ends_other(c0:symbol) +.decl path_starts_other(c0:symbol) +.decl cfg_bean_lifecycle_arg(c0:symbol,c1:symbol) +.decl cfg_registering_type_ann(c0:symbol,c1:symbol) +.decl cfg_registered_callback_name(c0:symbol,c1:symbol) +.decl cfg_servlet_registration(c0:symbol,c1:symbol) +.decl cfg_servlet_registration_bean(c0:symbol,c1:symbol) +.decl cfg_servlet_verb_method(c0:symbol,c1:symbol) +.decl cfg_jaxrs_invoke_verb(c0:symbol,c1:symbol) +.decl object_creation_named(c0:symbol,c1:symbol) +.decl registered_arg_type(c0:symbol,c1:symbol) +.decl path_join_demand(c0:symbol,c1:symbol) +.decl jaxrs_locator(c0:symbol,c1:symbol) +.decl locator_base(c0:symbol,c1:symbol) +.decl servlet_pattern(c0:symbol,c1:symbol) +.decl servlet_route_raw(c0:symbol,c1:symbol) +.decl jaxrs_target(c0:symbol,c1:symbol) +.decl jaxrs_builder(c0:symbol,c1:symbol) +.decl http_url_arg(c0:symbol,c1:symbol) +.decl cfg_registering_class_arg(c0:symbol,c1:symbol,c2:symbol) +.decl cfg_ann_name_of(c0:symbol,c1:symbol,c2:symbol) +.decl route_raw_own(c0:symbol,c1:symbol,c2:symbol) +.decl path_join(c0:symbol,c1:symbol,c2:symbol) +.decl servlet_decl_name(c0:symbol,c1:symbol,c2:symbol) +.decl servlet_mapped_pattern(c0:symbol,c1:symbol,c2:symbol) +.decl servlet_handler(c0:symbol,c1:symbol,c2:symbol) +.decl jaxrs_send(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl typed_simple_demand(c0:symbol) +.decl recv_type_simple(c0:symbol,c1:symbol) +.decl jaxrs_client_root(c0:symbol) +.decl locator_only_resource(c0:symbol) +.decl cfg_jaxrs_client_factory(c0:symbol) .decl sends_destination(c0:symbol,c1:symbol,c2:symbol) .decl route_base(c0:symbol,c1:symbol) .decl route_base_declared(c0:symbol) @@ -849,6 +929,24 @@ .decl pf_nested(c0:symbol,c1:symbol) .decl pf_direct(c0:symbol,c1:symbol) .decl persistence_unresolved(c0:symbol,c1:symbol) +.decl cfg_derived_query_prefix(c0:symbol) +.decl repo_entity(c0:symbol,c1:symbol) +.decl repo_entity_field(c0:symbol,c1:symbol,c2:symbol) +.decl repo_field_cap(c0:symbol,c1:symbol,c2:symbol) +.decl has_method_query_ann(c0:symbol) +.decl derived_candidate(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl derived_by_at(c0:symbol,c1:number) +.decl derived_criteria(c0:symbol,c1:symbol,c2:symbol) +.decl derived_start(c0:symbol,c1:number) +.decl derived_joiner(c0:symbol) +.decl derived_match(c0:symbol,c1:number,c2:number,c3:symbol) +.decl derived_ends(c0:symbol,c1:number) +.decl derived_longest(c0:symbol,c1:number,c2:number) +.decl pf_derived(c0:symbol,c1:symbol) +.decl orm_named_query(c0:symbol,c1:symbol,c2:symbol) +.decl cfg_orm_named_query_elem(c0:symbol) +.decl cfg_xml_param_marker_root(c0:symbol) +.decl xml_param_marker_file(c0:symbol) .decl cfg_method_query_ann(c0:symbol) .decl cfg_query_arg(c0:symbol) .decl query_padded(c0:symbol,c1:symbol) @@ -892,12 +990,9 @@ .decl gen_prefix(c0:symbol,c1:symbol) // (prov,accessorPrefix) .decl gen_prefix_ambiguous(c0:symbol) .decl gen_prefix_applies(c0:symbol,c1:symbol,c2:symbol) -.decl gen_prefix_key(c0:symbol) .decl gen_prefix_raw(c0:symbol,c1:symbol) -.decl gen_prefix_value(c0:symbol,c1:symbol) .decl gen_pfx_junk(c0:symbol) .decl gen_pfx_step(c0:symbol,c1:symbol) -.decl gen_accessor_src(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) // (prov,owner,fieldName,fieldHash,kind) .decl gen_return_type(c0:symbol,c1:symbol,c2:symbol) // (generatedGetterId, typeRef, type) .decl gen_capitalized(c0:symbol,c1:symbol) // (name, Name) .decl gen_accessor(c0:symbol,c1:symbol,c2:symbol,c3:symbol) // (prov,owner,methodName,arity) @@ -912,6 +1007,45 @@ .decl cfg_lib_bean_narrowing_enabled() .decl cfg_staged_lib_type(c0:symbol) .decl cfg_injectable_scope_type(c0:symbol) +.decl gen_field_with_ann(c0:symbol) // annotation on a FIELD declaring a wither +.decl gen_type_with_ann(c0:symbol) // annotation on a TYPE declaring a wither per field +.decl gen_logger_type(c0:symbol,c1:symbol) // (logger annotation, qualified type of the field) +.decl gen_static_arg(c0:symbol,c1:symbol) // (constructor annotation, element naming its static factory) +.decl gen_acc(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) // (prov,owner,fieldName,fieldType,fieldHash,kind) +.decl gen_cfg_key(c0:symbol) // a lombok.config key read here +.decl gen_cfg_value(c0:symbol,c1:symbol,c2:symbol) // (prov,key,rawValue) +.decl gen_cfg_clean(c0:symbol,c1:symbol,c2:symbol) // (prov,key,value with += / = peeled) +.decl gen_style_opt(c0:symbol) // an @Accessors element read here +.decl gen_acc_ann_field(c0:symbol,c1:symbol,c2:symbol) // (fieldHash,option,value) from @Accessors on the field +.decl gen_acc_ann_type(c0:symbol,c1:symbol,c2:symbol) // (ownerType,option,value) from @Accessors on the type +.decl gen_acc_field_has(c0:symbol,c1:symbol) +.decl gen_acc_type_has(c0:symbol,c1:symbol) +.decl gen_style(c0:symbol,c1:symbol,c2:symbol) // (fieldHash,option,value) as the processor decides it +.decl gen_fluent(c0:symbol) // fieldHash whose accessors are fluent +.decl gen_chain(c0:symbol) // fieldHash whose setter returns the owner +.decl gen_is_stripped(c0:symbol,c1:symbol,c2:symbol) // (prov,fieldHash,name) boolean isX field -> X +.decl gen_prop(c0:symbol,c1:symbol,c2:symbol) // (prov,fieldHash,propertyName) +.decl gen_cap_demand(c0:symbol) +.decl gen_acc_named(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) // (prov,owner,fieldHash,kind,methodName,arity) +.decl gen_member(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) // (prov,owner,ownerQualifiedName,name,arity) +.decl gen_field_type_q(c0:symbol,c1:symbol) // (generatedFieldId, qualified type name) +.decl gen_field_type(c0:symbol,c1:symbol,c2:symbol) // (generatedFieldId, prov, type) +.decl gen_field_recv(c0:symbol,c1:symbol) // (receiverExpr, generatedFieldId) +.decl gen_ctor_static(c0:symbol,c1:symbol,c2:symbol,c3:symbol) // (prov,owner,shape,factoryName) +.decl gen_ctor_arity(c0:symbol,c1:symbol,c2:number) // (owner,shape,arity) +.decl gen_ret_self(c0:symbol,c1:symbol) // (generatedMethodId, type it returns, no IR reference) +.decl gen_builder(c0:symbol,c1:symbol,c2:symbol) // (prov,owner,annotationHash) +.decl gen_builder_opt(c0:symbol,c1:symbol,c2:symbol) // (owner,element,value) +.decl gen_builder_opt_has(c0:symbol,c1:symbol) +.decl gen_builder_name(c0:symbol,c1:symbol,c2:symbol) // (owner, class|method|build, name) +.decl gen_builder_written(c0:symbol,c1:symbol,c2:symbol) // (owner, builderType, qname) the source writes the builder +.decl gen_builder_has_written(c0:symbol) +.decl gen_builder_type(c0:symbol,c1:symbol,c2:symbol,c3:symbol) // (prov,owner,builderType,builderQualifiedName) +.decl gen_builder_field_skip(c0:symbol) +.decl gen_builder_field(c0:symbol,c1:symbol,c2:symbol) // (prov,owner,fieldName) with a builder setter +.decl gen_delegate(c0:symbol,c1:symbol,c2:symbol) // (prov,owner,delegateFieldType) +.decl gen_delegate_kind(c0:symbol) +.decl gen_delegate_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol) // (delegateFieldType,name,arity,method) .decl gen_accessor_field(c0:symbol,c1:symbol) // call-edge-generation/event_dispatch.dl + knobs.dl (20): Spring application events (#1391) .decl cfg_event_publisher(c0:symbol,c1:symbol) @@ -966,3 +1100,26 @@ .decl twr_close_decl(c0:symbol,c1:symbol) .decl twr_close(c0:symbol,c1:symbol) .decl twr_close_count(c0:symbol,c1:number) +// call-edge-generation/callback_dispatch.dl +.decl cb_object_member(c0:symbol) +.decl cb_arg_type(c0:symbol,c1:symbol) +.decl cb_site_kind(c0:symbol) +.decl cb_client_site(c0:symbol) +.decl cb_handoff(c0:symbol,c1:symbol) +.decl cb_member(c0:symbol,c1:symbol,c2:symbol) +.decl cb_callback(c0:symbol,c1:symbol) +.decl cb_external_ancestor(c0:symbol) +.decl cb_writes_override(c0:symbol) +.decl cb_marker_type(c0:symbol) +.decl cb_direct_external(c0:symbol) +// call-edge-generation/exception_handlers.dl + knobs.dl (22): exception handler methods +.decl cfg_exh_ann(c0:symbol) +.decl cfg_exh_arg(c0:symbol) +.decl cfg_exh_advice_ann(c0:symbol) +.decl exh_ann_method(c0:symbol,c1:symbol) +.decl exh_names_classes(c0:symbol) +.decl exh_handles(c0:symbol,c1:symbol) +.decl exh_global(c0:symbol) +.decl exh_built(c0:symbol,c1:symbol,c2:symbol) +.decl exh_handles_built(c0:symbol,c1:symbol) +.decl exh_edge(c0:symbol,c1:symbol,c2: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/callbacks.dl b/graph/javascript/engine/call-edge-generation/callbacks.dl index 107842db..1ab0ee16 100644 --- a/graph/javascript/engine/call-edge-generation/callbacks.dl +++ b/graph/javascript/engine/call-edge-generation/callbacks.dl @@ -28,18 +28,59 @@ // ============================================================================ // ── callback_registered(CallExpr, CallbackMethod) ─────────────────────────── -callback_registered(ce, m) :- invocation_site(ce, _), call_arg(ce, _, arg), expr_value(arg, "func", m). +callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !collection_store_site(ce), + call_arg(ce, _, arg), expr_value(arg, "func", m). +// A PROJECT callee's own body calls what it is handed, so the edge from outside restates +// it — and only for a function WRITTEN at the argument. A parameter or loop variable passed +// on holds whatever the value flow says it may, and `for (const [pattern, handlers] of map)` +// gives the key every value too: `matches(pattern)` would register every handler. +callback_registered(ce, m) :- invocation_site(ce, _), call_has_client_target(ce), call_arg(ce, _, arg), + written_function(arg, m). +// written_function(Expr, Method): the expression names the function where it is written — +// a function or arrow literal, a function declaration or an import of one, a variable a +// literal initialises, a method read off a value (`this.onData`), or a call that returns +// one (`once(memoize(f))`, `fn.bind(this)`). +written_function(e, m) :- expr_introduces(_, m, e). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), function_binding_method(v, m). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + import_value(imp, "func", m). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_init(_, _, i, v), + written_function(i, m). +written_function(e, m) :- expr_kind(_, "PROPERTY_ACCESS", _, e), expr_value(e, "func", m). +written_function(e, m) :- expr_kind(_, "CALL", _, e), expr_value(e, "func", m). +// A Map or Set stores, finds and drops its argument and never calls it (`subs.add(h)`, +// `cache.get(key)`, `seen.has(entry)`); only `forEach` runs what it is handed. The +// function is reached where it is taken back out and called, not where it is stored. +collection_store_site(ce) :- call_site(_, ck, mn, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), mn != "forEach", + expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "coll", _). // A function handed over as a PROPERTY of an options object (`lib({ filter: f })`, // `opts.resolve = f; lib(opts)`, `https.request({ createConnection: f })`, // `new Transform({ transform })`) is a callback too (#643): the callee reads the property -// and invokes it, and nothing else in the project points into the function. Followed -// two levels (`{ hooks: { visit } }`), and only across the boundary: a callee that is -// NOT a project function (a staged library, the platform, an unknown, a dynamic call), -// because a project callee that reads the property has the ordinary edge from inside -// its body. Only object values carry it (below); an array, a collection, a string, a -// module, an instance and the platform are left out. +// and invokes it, and nothing else in the project points into the function. Only across +// the boundary: a callee that is NOT a project function (a staged library, the platform, +// an unknown, a dynamic call), because a project callee that reads the property has the +// ordinary edge from inside its body. +// Read off the SOURCE, not the value flow: the argument is an object literal or a variable +// one initialises, and the function is WRITTEN into it — a property value, a method of the +// literal, an assignment to a property of that variable, or the same one literal deeper +// (`{ hooks: { visit } }`). An argument whose abstract value merely MAY be such an object +// (a parameter, a property read, a call's result, a string key the flow over-approximated) +// hands nothing over, nor does a member holding a parameter or loop variable: +// `assert.equal(cfg.port, 1)`, `cache.get(key)` and `emit('error', { pattern })` would +// otherwise register every function of every object that value might be. callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !reflective_site(ce), - call_arg(ce, _, arg), expr_value(arg, k, i), options_value_kind(k), options_member_func(k, i, m). + !collection_store_site(ce), call_arg(ce, _, arg), options_argument(arg, l), options_written_member(l, m). +options_argument(l, l) :- expr_kind(_, "OBJECT_LITERAL", _, l). +options_argument(arg, l) :- expr_kind(_, "IDENTIFIER", _, arg), expr_binding(_, v, arg), var_init(_, _, l, v), + expr_kind(_, "OBJECT_LITERAL", _, l). +options_written_member(l, m) :- expr_kind(_, "OBJECT_LITERAL", _, l), literal_owns_method(l, m), + method_decl(_, _, k, _, _, _, _, m), !method_kind_is_accessor(k). +options_written_member(l, m) :- expr_child(_, l, "PROPERTY_VALUE", _, v), options_written_value(v, m). +options_written_member(l, m) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, t), + expr_kind(_, "PROPERTY_ACCESS", _, t), expr_child(_, t, "ACCESS_TARGET", _, r), expr_kind(_, "IDENTIFIER", _, r), + options_argument(r, l), expr_child(_, a, "ASSIGNMENT_VALUE", _, v), options_written_value(v, m). +options_written_value(v, m) :- written_function(v, m). +options_written_value(v, m) :- options_argument(v, l), options_written_member(l, m). // `Object.assign(dst, src)`, `Object.entries(o)`, `Object.setPrototypeOf(a, b)`, // `Reflect.ownKeys(o)`, `JSON.stringify(o)`: reflection over an object reads its // properties and never invokes them; an options-object edge there would be invented. @@ -49,23 +90,37 @@ reflective_ambient("Object"). reflective_ambient("Reflect"). reflective_ambient("JSON"). reflective_ambient("Array"). -// Two levels, each side materialised WITHOUT the name wildcard (#671): joining -// prop_value with itself through `_` in the name column left neither side a -// prefix index for the other's lookup, and on lodash (26M prop_value tuples after -// its mixin copies every function onto every object) that one join took 43 of a -// 51-minute solve to produce 16.8K rows. The projections below are deduplicated -// per (object, member value), and each join is a prefix lookup. -options_member_func(k, i, m) :- options_member_func_own(k, i, m). -options_member_func(k, i, m) :- options_object_member(k, i, k1, i1), options_member_func_own(k1, i1, m). -options_member_func_own(k, i, m) :- prop_value(k, i, _, "func", m). -options_object_member(k, i, k1, i1) :- prop_value(k, i, _, k1, i1), options_value_kind(k1). -// Object values only: an INSTANCE passed to the platform is data (`arr.push(this)`, -// `Promise.resolve(w)`, `items.map(fn, this)`), and registering every method it has -// would invent an edge per method at every such site. -options_value_kind("obj"). call_has_client_target(ce) :- expr_resolves_to_method(ce, m), method_prov(m, "client"). call_has_client_target(ce) :- new_constructs(ce, t), type_decl("client", _, _, _, _, t). +// A function WRAPPED BY A PACKAGE CALL and kept in a binding: `export const plugin = fp(async (app) => …)`, +// `const ext = Prisma.defineExtension((client) => …)`, then `app.register(plugin)`, `client.$extends(ext)`. +// The package returns the function it was handed, or one that runs it, and nothing in the project says +// which, so the binding had no value and the registration reached nothing, although the same literal +// handed to it bare is registered. The wrapping site is a value of its own, ("libwrap", site), carried +// wherever a value goes (a const, an import, an export), and a registration handed it registers what the +// site was handed. The site is recognised syntactically, by a callee rooted at a binding imported from a +// package, so the value stays below the resolver's negations; a project wrapper is followed through its +// body instead (value-flow.dl, "wrap"). +lib_rooted(e) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + import_decl(_, _, _, _, _, _, out, _, imp), import_outcome_is_package(out). +lib_rooted(e) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_child(_, e, "ACCESS_TARGET", _, r), lib_rooted(r). +import_outcome_is_package("RESOLVED_EXTERNAL"). +import_outcome_is_package("UNRESOLVED_MISSING"). +access_kind_reads_member("PROPERTY_ACCESS"). +access_kind_reads_member("OPTIONAL_ACCESS"). +lib_wrap_site(s) :- call_site(_, ck, _, _, _, _, s, _, _), call_kind_is_callee_form(ck), + expr_child(_, s, "CALLEE", _, c), lib_rooted(c). +lib_wrap_site(s) :- call_site(_, ck, _, "SYNTACTIC", _, _, s, _, _), call_kind_is_member_form(ck), + expr_child(_, s, "RECEIVER", _, r), lib_rooted(r). +// Handed over by NAME: `register(wrap(f))` needs nothing new, since the inner site already registers f +// from the same caller. The value is not a callee (callee-resolution.dl): what a call of it runs is still +// unknown, and says so. +expr_value(s, "libwrap", s) :- lib_wrap_site(s), call_arg(s, _, a), expr_value(a, "func", _). +callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !reflective_site(ce), + call_arg(ce, _, arg), !expr_kind(_, "CALL", _, arg), + expr_value(arg, "libwrap", s), call_arg(s, _, a), expr_value(a, "func", m). + // A listener runs with the EMITTER as `this` (`e.on('x', function () { this.other(); })`). this_value(m, k, i) :- event_handler(k, i, _, m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _), k != "module". diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index 34fbd6b8..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 @@ -107,8 +137,62 @@ module_variable_from_call(v) :- var_init("client", _, e, v), expr_kind(_, k, _, !variable_reassigned(v), !call_passes_function(e). call_passes_function(e) :- call_arg(e, _, a), expr_value(a, "func", _). call_passes_function(e) :- call_arg(e, _, a), expr_introduces(_, _, a). +// `promisify(store.find.bind(store))`: a `.bind` always evaluates to a function, typed +// receiver or not. A bound platform function (`Math.max.bind(Math)`) is the platform's. +call_passes_function(e) :- call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _), + expr_child(_, a, "CALLEE", _, f), !bound_platform_function(f). +bound_platform_function(f) :- expr_kind(_, "PROPERTY_ACCESS", _, f), expr_child(_, f, "ACCESS_TARGET", _, r), + expr_value(r, "ambient", _). +// A holder whose value is what a PLATFORM call returned when handed a project function +// (`this.find = promisify(s.find.bind(s))`, `const f = util.callbackify(g)`): the value +// is a wrapper the platform made around that function, and calling it runs the +// function. The platform value on the holder made the call an ambient terminal, a +// claimed correct end, and path said "independent" of the very method the wrapper +// runs. A platform value made from no project function (`promisify(setTimeout)`) +// keeps its platform reading. +// When the graph knows the function the wrapper was made from (a function value, or +// the method a `.bind` site resolves to), the call runs it: one of a set, beside the +// platform row the call keeps. Only when it knows none is the callee an open value. +unresolved_value_callee(ce, "field") :- wrapper_holder_call(ce, val), !wrapper_call_target(ce, _), made_from_function(val), + field_call(ce, _, _, _). +unresolved_value_callee(ce, "module_variable") :- wrapper_holder_call(ce, e), !wrapper_call_target(ce, _), made_from_function(e), + !field_call(ce, _, _, _). +wrapper_holder_call(ce, val) :- field_call(ce, k, t, n), !call_resolved(ce), field_assignment(k, t, n, val). +wrapper_holder_call(ce, e) :- value_callee_unresolved(ce, c), expr_binding(_, v, c), + var_decl("client", _, _, _, _, _, v), !var_owner_method("client", _, v), !var_import(_, _, v), !expr_param(_, _, c), + var_init("client", _, e, v), !variable_reassigned(v). +made_from_function(e) :- expr_kind(_, "CALL", _, e), expr_value(e, "ambient", _), call_passes_function(e). +wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), expr_value(a, "func", m). +wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _), + expr_resolves_to_method(a, m). +wrapper_call_target(ce, m) :- wrapper_holder_call(ce, e), wrapper_runs(e, m), call_target_count(ce, 0). +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/call-edge-generation/jsx.dl b/graph/javascript/engine/call-edge-generation/jsx.dl index f3c31375..67a6c893 100644 --- a/graph/javascript/engine/call-edge-generation/jsx.dl +++ b/graph/javascript/engine/call-edge-generation/jsx.dl @@ -26,7 +26,10 @@ // A tag bound to a component WRAPPER (`memo(C)`, `forwardRef(fn)`, `observer(C)`, // `withRouter(C)`) or a LOADER (`lazy(() => import("./x"))`) renders what the wrapper // was handed or what the loader settles to — see "wrapped and lazy components" below. -// Not modelled: `connect(…)(C)` and other curried wrappers. +// Redux's curried `connect(…)(C)` is such a wrapper, and it also hands C the props its +// map functions return; React context hands `useContext(Ctx)` the provider's `value` +// — see "props that come from outside the element" below. Not modelled: other curried +// wrappers. // ============================================================================ // ── the element and what its tag holds ────────────────────────────────────── @@ -59,12 +62,32 @@ jsx_loader_name("lazy"). jsx_loader_name("dynamic"). jsx_loader_name("loadable"). jsx_component_loader(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), jsx_loader_name(n). +// A Vue DEFINER — `defineComponent({ setup() {…} })` (Nuxt: `defineNuxtComponent`): the +// tag invokes the options object's `setup` with its props, or its `render` when there is +// no setup; a render function setup returns is reached from setup's body. The function +// form, `defineComponent((props) => () => …)`, is argument 0 itself, as for memo. Named by +// the definer, so the same object handed to any other call stays out. +jsx_vue_definer_name("defineComponent"). +jsx_vue_definer_name("defineNuxtComponent"). +jsx_vue_definer(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), jsx_vue_definer_name(n). +// Redux `connect(mapState, mapDispatch)(C)`: the OUTER call is the wrapper, its argument +// 0 the component. Keyed on the callee being a call of `connect`, so a plain +// `connect(db)` or a `connect(…)` whose result is not called again stays out. +jsx_connect_call(w, inner) :- expr_kind(_, "CALL", _, w), expr_child(_, w, "CALLEE", _, inner), + expr_kind(_, "CALL", _, inner), call_site("client", _, "connect", _, _, _, inner, _, _). +jsx_component_wrapper(w) :- jsx_connect_call(w, _). +// React `createContext(…)`: not a wrapper, but it reaches `` and +// `useContext(Ctx)` the ways a component reaches a tag, so jsx_holds follows it too. +// No jsx_call_renders rule reads it: holding a context renders nothing. +jsx_context_create(c) :- expr_kind(_, "CALL", _, c), call_site("client", _, "createContext", _, _, _, c, _, _). // jsx_holds(Expr, Call) — the wrapper or loader call an expression holds, followed the // ways a component reaches a tag: the call itself, a const initialised with it, a named // or default import of an export that holds it, and a re-export of one. jsx_holds(w, w) :- jsx_component_wrapper(w). jsx_holds(w, w) :- jsx_component_loader(w). +jsx_holds(w, w) :- jsx_vue_definer(w). +jsx_holds(c, c) :- jsx_context_create(c). jsx_holds(e, w) :- expr_binding(_, v, e), jsx_var_holds(v, w). jsx_var_holds(v, w) :- var_init(_, _, e, v), !var_binding_form(_, "OBJECT_PATTERN", v), !var_binding_form(_, "ARRAY_PATTERN", v), jsx_holds(e, w). @@ -97,6 +120,39 @@ jsx_call_renders(w, k, i) :- jsx_loader_settles(w, "obj", o), prop_value("obj", jsx_call_renders(w, k, i) :- jsx_loader_settles(w, k, i), jsx_component_kind(k). jsx_component_kind("func"). jsx_component_kind("ctor"). +// A Vue definer: the options object's `setup`, else its `render` — one target, not both. +// "Has a setup" is read off the literal's own keys, not the value layer, which this +// relation feeds. +jsx_call_renders(w, "func", m) :- jsx_vue_definer(w), call_arg(w, 0, a), expr_value(a, "obj", o), + prop_value("obj", o, "setup", "func", m). +jsx_call_renders(w, "func", m) :- jsx_vue_definer(w), call_arg(w, 0, a), expr_value(a, "obj", o), + !jsx_vue_has_setup(o), prop_value("obj", o, "render", "func", m). +jsx_call_renders(w, "func", f) :- jsx_vue_definer(w), call_arg(w, 0, a), expr_value(a, "func", f). +jsx_vue_has_setup(o) :- expr_kind(_, "OBJECT_LITERAL", _, o), expr_child(_, o, "PROPERTY_KEY", _, key), expr_name(_, "setup", key). +jsx_vue_has_setup(o) :- literal_owns_method(o, m), method_decl(_, "setup", _, _, _, _, _, m). + +// ── Vue single-file components ────────────────────────────────────────────── +// A `.vue` template tag, ``, reaches this file as a JSX_ELEMENT (the parser +// writes it into the component's virtual script), so everything above applies to it. +// A definer call renders when its argument is a function or names setup/render — read +// off the literal's own keys, as jsx_vue_has_setup is. +jsx_vue_option(a, n) :- literal_owns_method(a, m), method_decl(_, n, _, _, _, _, _, m). +jsx_vue_option(a, n) :- expr_child(_, a, "PROPERTY_KEY", _, key), expr_name(_, n, key). +jsx_vue_definer_renders(w) :- jsx_vue_definer(w), call_arg(w, 0, a), expr_kind(_, "FUNCTION_EXPRESSION", _, a). +jsx_vue_definer_renders(w) :- jsx_vue_definer(w), call_arg(w, 0, a), jsx_vue_option(a, "setup"). +jsx_vue_definer_renders(w) :- jsx_vue_definer(w), call_arg(w, 0, a), jsx_vue_option(a, "render"). + +// A single-file component, `import Child from "./Child.vue"`: its script is the +// module's top-level code — a ` + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/Child.vue b/graph/test/javascript/cases/72-vue-component-tag/src/components/Child.vue new file mode 100644 index 00000000..4649dc06 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/Child.vue @@ -0,0 +1,4 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/Defined.vue b/graph/test/javascript/cases/72-vue-component-tag/src/components/Defined.vue new file mode 100644 index 00000000..64fda779 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/Defined.vue @@ -0,0 +1,8 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/Legacy.vue b/graph/test/javascript/cases/72-vue-component-tag/src/components/Legacy.vue new file mode 100644 index 00000000..0b8279d6 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/Legacy.vue @@ -0,0 +1,6 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/MyWidget.vue b/graph/test/javascript/cases/72-vue-component-tag/src/components/MyWidget.vue new file mode 100644 index 00000000..01e94047 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/MyWidget.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/badge.js b/graph/test/javascript/cases/72-vue-component-tag/src/components/badge.js new file mode 100644 index 00000000..91986299 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/badge.js @@ -0,0 +1 @@ +export function Badge() { return "b"; } diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/components/store.js b/graph/test/javascript/cases/72-vue-component-tag/src/components/store.js new file mode 100644 index 00000000..7e5362e8 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/components/store.js @@ -0,0 +1 @@ +export default { items: [] }; diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/views/Parent.vue b/graph/test/javascript/cases/72-vue-component-tag/src/views/Parent.vue new file mode 100644 index 00000000..16cdfe43 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/views/Parent.vue @@ -0,0 +1,24 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/views/Shell.vue b/graph/test/javascript/cases/72-vue-component-tag/src/views/Shell.vue new file mode 100644 index 00000000..d9c302fc --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/views/Shell.vue @@ -0,0 +1,12 @@ + + diff --git a/graph/test/javascript/cases/72-vue-component-tag/src/vite.config.js b/graph/test/javascript/cases/72-vue-component-tag/src/vite.config.js new file mode 100644 index 00000000..798b97d7 --- /dev/null +++ b/graph/test/javascript/cases/72-vue-component-tag/src/vite.config.js @@ -0,0 +1,4 @@ +import { fileURLToPath, URL } from "node:url"; +export default { + resolve: { alias: { "@": fileURLToPath(new URL("./components", import.meta.url)) } }, +}; diff --git a/graph/test/javascript/cases/73-bound-function-identity/src/bound.js b/graph/test/javascript/cases/73-bound-function-identity/src/bound.js new file mode 100644 index 00000000..40374a11 --- /dev/null +++ b/graph/test/javascript/cases/73-bound-function-identity/src/bound.js @@ -0,0 +1,50 @@ +'use strict'; +const { promisify } = require('util'); + +class Repo { + append(x) { return x; } + find(k) { return k; } +} +class Other { + append(x) { return x; } +} + +// (a) a field or variable assigned from x.m.bind(x) is x.m +class Service { + constructor(r) { + this.repo = r; + this.add = this.repo.append.bind(this.repo); + this.lookup = promisify(r.find.bind(r)); + } + go() { return this.add(1); } + get(k) { return this.lookup(k); } +} +function makeService() { return new Service(new Repo()); } +const repo = new Repo(); +const bound = repo.append.bind(repo); +function viaVariable() { return bound(2); } +const wrapped = promisify(repo.find.bind(repo)); +function viaPromisified() { return wrapped('k'); } +const util = require('util'); +const viaNamespace = util.promisify(repo.find.bind(repo)); +function viaNamespaceCall() { return viaNamespace('n'); } + +// (b) a function bound to an object literal sees its keys through `this` +function onEvent(e) { return this.repo.append(e); } +const handler = onEvent.bind({ repo: new Repo() }); +function fire() { return handler(1); } +const handlers = { + created: function onCreated(e) { return this.store.append(e); }, +}; +const onCreatedBound = handlers.created.bind({ store: new Other() }); + +// controls: none of these changes +function unbound(e) { return this.repo.append(e); } +function callsUnbound() { return unbound.call({ repo: new Repo() }, 1); } +const snapshot = repo.find.bind(null); +function openBind(fn) { const g = fn.bind(repo); return g(); } +const own = { promisify(f) { return () => f; } }; +const notUtil = own.promisify(repo.find); +function viaOwnPromisify() { return notUtil(); } + +module.exports = { makeService, viaVariable, viaPromisified, fire, onCreatedBound, callsUnbound, snapshot, openBind, viaNamespaceCall, viaOwnPromisify }; diff --git a/graph/test/javascript/cases/74-orm-model-factory/src/models.js b/graph/test/javascript/cases/74-orm-model-factory/src/models.js new file mode 100644 index 00000000..c5b18612 --- /dev/null +++ b/graph/test/javascript/cases/74-orm-model-factory/src/models.js @@ -0,0 +1,32 @@ +'use strict'; +// A model made by an ORM factory, `connection.model(name, schema)`, is a class the +// package builds at runtime from the schema: its statics are the functions written +// onto `schema.statics` (or handed to `schema.static(...)`), and a document it +// constructs has the schema's methods. The package is not installed, so nothing in +// the tree declares the model; the schema argument is what carries its members. +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const docSchema = new Schema({ title: String }); +docSchema.statics.findLive = function () { return this.find({}); }; +docSchema.static('claim', function () { return this.findLive(); }); +docSchema.static({ purge() { return 0; } }); +docSchema.methods.toRecord = function () { return this.title; }; +docSchema.method('touch', function () { return this.toRecord(); }); + +// The options form, on the package's namespace. +const jobSchema = new mongoose.Schema({}, { statics: { due() { return 1; } } }); + +// Control: a schema-like object from a package that is not modelled stays unknown. +const { Schema: OtherSchema } = require('other-orm'); +const otherSchema = new OtherSchema({}); +otherSchema.statics.findLive = function () { return 2; }; + +function registerModels(connection) { + return { + Doc: connection.models.Doc || connection.model('Doc', docSchema), + Job: mongoose.model('Job', jobSchema), + Other: connection.model('Other', otherSchema), + }; +} +module.exports = { registerModels }; diff --git a/graph/test/javascript/cases/74-orm-model-factory/src/repo.js b/graph/test/javascript/cases/74-orm-model-factory/src/repo.js new file mode 100644 index 00000000..55f3b77d --- /dev/null +++ b/graph/test/javascript/cases/74-orm-model-factory/src/repo.js @@ -0,0 +1,31 @@ +'use strict'; +const { registerModels } = require('./models'); + +class DocRepository { + constructor(models) { this.Doc = models.Doc; this.Job = models.Job; this.Other = models.Other; } + list() { return this.Doc.findLive(); } + take() { return this.Doc.claim(); } + purge() { return this.Doc.purge(); } + due() { return this.Job.due(); } + record() { return new this.Doc({}).touch(); } + save() { return this.Doc.create({}); } // the package's own member: stays unknown + other() { return this.Other.findLive(); } // control: the unmodelled package +} + +// Control: an unrelated repository with same-named methods, reached by nothing above. +class MemoryRepository { + findLive() { return []; } + claim() { return null; } +} + +// Control: a project `.model(name, x)` whose second argument is not a schema keeps +// its own return value. +const registry = { model(name, def) { return def; } }; +const plain = registry.model('x', { findLive() { return 3; } }); + +function start(connection) { + const repo = new DocRepository(registerModels(connection)); + return [repo.list(), repo.take(), repo.purge(), repo.due(), repo.record(), repo.save(), repo.other(), + plain.findLive(), new MemoryRepository()]; +} +module.exports = { start }; 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 f261bb39..1d54eea4 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.diag @@ -1,6 +1,10 @@ +import_cause main.js:17:16 assert builtin 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 @@ -8,6 +12,29 @@ unresolved main.js:12:86 METHOD_CALL destroy no_target unresolved main.js:12:86 METHOD_CALL on no_target unresolved main.js:12:86 METHOD_CALL request no_target unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target +unresolved main.js:18:15 CONSTRUCTOR_CALL Map no_target +unresolved main.js:19:14 CONSTRUCTOR_CALL Set no_target +unresolved main.js:20:65 METHOD_CALL keys no_target +unresolved main.js:20:95 METHOD_CALL default receiver_untyped +unresolved main.js:23:31 METHOD_CALL get no_target +unresolved main.js:24:32 METHOD_CALL has no_target +unresolved main.js:25:74 METHOD_CALL add no_target +unresolved main.js:25:89 METHOD_CALL set no_target +unresolved main.js:27:25 METHOD_CALL equal no_target +unresolved main.js:27:95 FUNCTION_CALL walk callee_untyped +unresolved main.js:28:23 FUNCTION_CALL setTimeout no_target +unresolved main.js:28:50 METHOD_CALL forEach no_target +unresolved main.js:32:19 CONSTRUCTOR_CALL Map no_target +unresolved main.js:33:29 METHOD_CALL get no_target +unresolved main.js:33:61 CONSTRUCTOR_CALL Set no_target +unresolved main.js:33:72 METHOD_CALL set no_target +unresolved main.js:33:95 METHOD_CALL add no_target +unresolved main.js:35:122 METHOD_CALL emit no_target +unresolved main.js:37:131 CONSTRUCTOR_CALL no_target +unresolved main.js:41:16 FUNCTION_CALL walk callee_untyped +unresolved main.js:42:18 FUNCTION_CALL walk callee_untyped +unresolved main.js:43:25 METHOD_CALL register receiver_untyped +unresolved main.js:43:48 METHOD_CALL register receiver_untyped unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped unresolved main.js:9:42 METHOD_CALL map no_target value_callee main.js:8:38 cb parameter diff --git a/graph/test/javascript/expected/34-options-object-callbacks.edges b/graph/test/javascript/expected/34-options-object-callbacks.edges index 04390e7a..b6c226f6 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.edges @@ -12,12 +12,55 @@ main.js:12:86 METHOD_CALL https.request(o).on('error', () => {}).destroy -> am main.js:13:29 CONSTRUCTOR_CALL Transform -> ambient_terminal - main.js:13:29 CONSTRUCTOR_CALL Transform -> callback_registered main.js:8:1 transform main.js:14:32 FUNCTION_CALL localWalk -> known_edge main.js:9:1 localWalk -main.js:15:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct -main.js:15:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar -main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform -main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor -main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject -main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main +main.js:18:15 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:19:14 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:20:65 METHOD_CALL Object.keys -> ambient_terminal - +main.js:20:95 METHOD_CALL schema[k].default -> ambiguous_unknown - +main.js:22:13 FUNCTION_CALL defineConfig -> known_edge main.js:20:1 defineConfig +main.js:23:31 METHOD_CALL cache.get -> ambient_terminal - +main.js:24:32 METHOD_CALL subs.has -> ambient_terminal - +main.js:25:74 METHOD_CALL subs.add -> ambient_terminal - +main.js:25:89 METHOD_CALL cache.set -> ambient_terminal - +main.js:27:25 METHOD_CALL assert.equal -> ambient_terminal - +main.js:27:60 FUNCTION_CALL lookup -> known_edge main.js:23:1 lookup +main.js:27:74 FUNCTION_CALL known -> known_edge main.js:24:1 known +main.js:27:95 FUNCTION_CALL walk -> ambiguous_unknown - +main.js:28:23 FUNCTION_CALL setTimeout -> ambient_terminal - +main.js:28:23 FUNCTION_CALL setTimeout -> callback_registered main.js:28:34 +main.js:28:40 FUNCTION_CALL keep -> known_edge main.js:5:1 keep +main.js:28:50 METHOD_CALL [1].forEach -> ambient_terminal - +main.js:28:50 METHOD_CALL [1].forEach -> callback_registered main.js:6:1 visit +main.js:32:19 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:33:29 METHOD_CALL byPattern.get -> ambient_terminal - +main.js:33:61 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:33:72 METHOD_CALL byPattern.set -> ambient_terminal - +main.js:33:95 METHOD_CALL s.add -> ambient_terminal - +main.js:35:122 METHOD_CALL bus.emit -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> multi_inferred main.js:26:1 onUpdated +main.js:35:84 FUNCTION_CALL matchesKey -> known_edge main.js:34:1 matchesKey +main.js:36:20 FUNCTION_CALL on -> callback_registered main.js:26:1 onUpdated +main.js:36:20 FUNCTION_CALL on -> known_edge main.js:33:1 on +main.js:36:40 FUNCTION_CALL deliver -> known_edge main.js:35:1 deliver +main.js:37:115 FUNCTION_CALL viaTimer -> known_edge main.js:28:1 viaTimer +main.js:37:127 FUNCTION_CALL bus -> known_edge main.js:36:1 bus +main.js:37:131 CONSTRUCTOR_CALL (require('events')) -> ambient_terminal - +main.js:37:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct +main.js:37:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar +main.js:37:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform +main.js:37:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor +main.js:37:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject +main.js:37:79 FUNCTION_CALL check -> known_edge main.js:27:1 check +main.js:37:85 FUNCTION_CALL subscribe -> callback_registered main.js:26:1 onUpdated +main.js:37:85 FUNCTION_CALL subscribe -> known_edge main.js:25:1 subscribe +main.js:38:1 FUNCTION_CALL main -> known_edge main.js:37:1 main +main.js:41:16 FUNCTION_CALL walk -> ambiguous_unknown - +main.js:41:16 FUNCTION_CALL walk -> callback_registered main.js:41:21 +main.js:41:33 FUNCTION_CALL migrate -> known_edge main.js:40:1 migrate +main.js:42:18 FUNCTION_CALL walk -> ambiguous_unknown - +main.js:43:25 METHOD_CALL walk.register -> ambiguous_unknown - +main.js:43:25 METHOD_CALL walk.register -> callback_registered main.js:41:21 +main.js:43:48 METHOD_CALL walk.register -> ambiguous_unknown - main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown - main.js:9:42 METHOD_CALL items.map -> ambient_terminal - main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 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 99d2f7ba..e30f6720 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag @@ -1,11 +1,35 @@ +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 unresolved main.js:12:86 METHOD_CALL on no_target unresolved main.js:12:86 METHOD_CALL request no_target unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target +unresolved main.js:18:15 CONSTRUCTOR_CALL Map no_target +unresolved main.js:19:14 CONSTRUCTOR_CALL Set no_target +unresolved main.js:20:65 METHOD_CALL keys no_target +unresolved main.js:20:95 METHOD_CALL default receiver_untyped +unresolved main.js:23:31 METHOD_CALL get no_target +unresolved main.js:24:32 METHOD_CALL has no_target +unresolved main.js:25:74 METHOD_CALL add no_target +unresolved main.js:25:89 METHOD_CALL set no_target +unresolved main.js:27:25 METHOD_CALL equal no_target +unresolved main.js:28:23 FUNCTION_CALL setTimeout no_target +unresolved main.js:28:50 METHOD_CALL forEach no_target +unresolved main.js:32:19 CONSTRUCTOR_CALL Map no_target +unresolved main.js:33:29 METHOD_CALL get no_target +unresolved main.js:33:61 CONSTRUCTOR_CALL Set no_target +unresolved main.js:33:72 METHOD_CALL set no_target +unresolved main.js:33:95 METHOD_CALL add no_target +unresolved main.js:35:122 METHOD_CALL emit no_target +unresolved main.js:37:131 CONSTRUCTOR_CALL no_target +unresolved main.js:43:25 METHOD_CALL register member_absent +unresolved main.js:43:48 METHOD_CALL register member_absent unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped unresolved main.js:9:42 METHOD_CALL map no_target value_callee main.js:8:38 cb parameter diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges index f5582b13..5af7f9c6 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges @@ -12,12 +12,55 @@ main.js:12:86 METHOD_CALL https.request(o).on('error', () => {}).destroy -> am main.js:13:29 CONSTRUCTOR_CALL Transform -> ambient_terminal - main.js:13:29 CONSTRUCTOR_CALL Transform -> callback_registered main.js:8:1 transform main.js:14:32 FUNCTION_CALL localWalk -> known_edge main.js:9:1 localWalk -main.js:15:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct -main.js:15:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar -main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform -main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor -main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject -main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main +main.js:18:15 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:19:14 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:20:65 METHOD_CALL Object.keys -> ambient_terminal - +main.js:20:95 METHOD_CALL schema[k].default -> ambiguous_unknown - +main.js:22:13 FUNCTION_CALL defineConfig -> known_edge main.js:20:1 defineConfig +main.js:23:31 METHOD_CALL cache.get -> ambient_terminal - +main.js:24:32 METHOD_CALL subs.has -> ambient_terminal - +main.js:25:74 METHOD_CALL subs.add -> ambient_terminal - +main.js:25:89 METHOD_CALL cache.set -> ambient_terminal - +main.js:27:25 METHOD_CALL assert.equal -> ambient_terminal - +main.js:27:60 FUNCTION_CALL lookup -> known_edge main.js:23:1 lookup +main.js:27:74 FUNCTION_CALL known -> known_edge main.js:24:1 known +main.js:27:95 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk +main.js:28:23 FUNCTION_CALL setTimeout -> ambient_terminal - +main.js:28:23 FUNCTION_CALL setTimeout -> callback_registered main.js:28:34 +main.js:28:40 FUNCTION_CALL keep -> known_edge main.js:5:1 keep +main.js:28:50 METHOD_CALL [1].forEach -> ambient_terminal - +main.js:28:50 METHOD_CALL [1].forEach -> callback_registered main.js:6:1 visit +main.js:32:19 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:33:29 METHOD_CALL byPattern.get -> ambient_terminal - +main.js:33:61 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:33:72 METHOD_CALL byPattern.set -> ambient_terminal - +main.js:33:95 METHOD_CALL s.add -> ambient_terminal - +main.js:35:122 METHOD_CALL bus.emit -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> multi_inferred main.js:26:1 onUpdated +main.js:35:84 FUNCTION_CALL matchesKey -> known_edge main.js:34:1 matchesKey +main.js:36:20 FUNCTION_CALL on -> callback_registered main.js:26:1 onUpdated +main.js:36:20 FUNCTION_CALL on -> known_edge main.js:33:1 on +main.js:36:40 FUNCTION_CALL deliver -> known_edge main.js:35:1 deliver +main.js:37:115 FUNCTION_CALL viaTimer -> known_edge main.js:28:1 viaTimer +main.js:37:127 FUNCTION_CALL bus -> known_edge main.js:36:1 bus +main.js:37:131 CONSTRUCTOR_CALL (require('events')) -> ambient_terminal - +main.js:37:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct +main.js:37:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar +main.js:37:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform +main.js:37:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor +main.js:37:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject +main.js:37:79 FUNCTION_CALL check -> known_edge main.js:27:1 check +main.js:37:85 FUNCTION_CALL subscribe -> callback_registered main.js:26:1 onUpdated +main.js:37:85 FUNCTION_CALL subscribe -> known_edge main.js:25:1 subscribe +main.js:38:1 FUNCTION_CALL main -> known_edge main.js:37:1 main +main.js:41:16 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk +main.js:41:16 FUNCTION_CALL walk -> callback_registered main.js:41:21 +main.js:41:33 FUNCTION_CALL migrate -> known_edge main.js:40:1 migrate +main.js:42:18 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk +main.js:43:25 METHOD_CALL walk.register -> ambiguous_unknown - +main.js:43:25 METHOD_CALL walk.register -> callback_registered main.js:41:21 +main.js:43:48 METHOD_CALL walk.register -> ambiguous_unknown - main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown - main.js:9:42 METHOD_CALL items.map -> ambient_terminal - main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.oracle b/graph/test/javascript/expected/34-options-object-callbacks.oracle index be68d152..ef82a93c 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.oracle +++ b/graph/test/javascript/expected/34-options-object-callbacks.oracle @@ -1,8 +1,33 @@ main.js:14:32 FUNCTION_CALL localWalk EXACT main.js:9:1 -main.js:15:19 FUNCTION_CALL direct EXACT main.js:10:1 -main.js:15:29 FUNCTION_CALL viaVar EXACT main.js:11:1 -main.js:15:39 FUNCTION_CALL viaPlatform EXACT main.js:12:1 -main.js:15:54 FUNCTION_CALL viaCtor EXACT main.js:13:1 -main.js:15:65 FUNCTION_CALL viaProject EXACT main.js:14:1 -main.js:16:1 FUNCTION_CALL main EXACT main.js:15:1 +main.js:18:15 CONSTRUCTOR_CALL Map LIB_AMBIENT_OK +main.js:19:14 CONSTRUCTOR_CALL Set LIB_AMBIENT_OK +main.js:20:65 METHOD_CALL keys LIB_AMBIENT_OK +main.js:22:13 FUNCTION_CALL defineConfig EXACT main.js:20:1 +main.js:23:31 METHOD_CALL get LIB_AMBIENT_OK +main.js:24:32 METHOD_CALL has LIB_AMBIENT_OK +main.js:25:74 METHOD_CALL add LIB_AMBIENT_OK +main.js:25:89 METHOD_CALL set LIB_AMBIENT_OK +main.js:27:60 FUNCTION_CALL lookup EXACT main.js:23:1 +main.js:27:74 FUNCTION_CALL known EXACT main.js:24:1 +main.js:28:23 FUNCTION_CALL setTimeout LIB_AMBIENT_OK +main.js:28:40 FUNCTION_CALL keep EXACT main.js:5:1 +main.js:28:50 METHOD_CALL forEach LIB_AMBIENT_OK +main.js:32:19 CONSTRUCTOR_CALL Map LIB_AMBIENT_OK +main.js:33:29 METHOD_CALL get LIB_AMBIENT_OK +main.js:33:61 CONSTRUCTOR_CALL Set LIB_AMBIENT_OK +main.js:33:72 METHOD_CALL set LIB_AMBIENT_OK +main.js:35:84 FUNCTION_CALL matchesKey EXACT main.js:34:1 +main.js:36:20 FUNCTION_CALL on EXACT main.js:33:1 +main.js:36:40 FUNCTION_CALL deliver EXACT main.js:35:1 +main.js:37:115 FUNCTION_CALL viaTimer EXACT main.js:28:1 +main.js:37:127 FUNCTION_CALL bus EXACT main.js:36:1 +main.js:37:19 FUNCTION_CALL direct EXACT main.js:10:1 +main.js:37:29 FUNCTION_CALL viaVar EXACT main.js:11:1 +main.js:37:39 FUNCTION_CALL viaPlatform EXACT main.js:12:1 +main.js:37:54 FUNCTION_CALL viaCtor EXACT main.js:13:1 +main.js:37:65 FUNCTION_CALL viaProject EXACT main.js:14:1 +main.js:37:79 FUNCTION_CALL check EXACT main.js:27:1 +main.js:37:85 FUNCTION_CALL subscribe EXACT main.js:25:1 +main.js:38:1 FUNCTION_CALL main EXACT main.js:37:1 +main.js:41:33 FUNCTION_CALL migrate EXACT main.js:40:1 # defects: 0 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/50-enumerated-copy-keys.edges b/graph/test/javascript/expected/50-enumerated-copy-keys.edges index 3b463f44..6918da3e 100644 --- a/graph/test/javascript/expected/50-enumerated-copy-keys.edges +++ b/graph/test/javascript/expected/50-enumerated-copy-keys.edges @@ -12,8 +12,6 @@ main.js:19:31 METHOD_CALL keys(obj2).forEach -> callback_registered main.js:1 main.js:20:12 FUNCTION_CALL extend -> known_edge main.js:19:1 extend main.js:21:24 METHOD_CALL d4.alpha -> known_edge main.js:7:15 alpha main.js:25:55 FUNCTION_CALL ownKeys -> ambient_terminal - -main.js:25:55 FUNCTION_CALL ownKeys -> callback_registered main.js:7:15 alpha -main.js:25:55 FUNCTION_CALL ownKeys -> callback_registered main.js:7:42 beta main.js:26:12 FUNCTION_CALL assign -> known_edge main.js:25:1 assign main.js:27:24 METHOD_CALL d5.beta -> known_edge main.js:7:42 beta main.js:28:16 METHOD_CALL Object.getOwnPropertyNames -> ambient_terminal - 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/69-vue-sfc.edges b/graph/test/javascript/expected/69-vue-sfc.edges index d7efd225..47623a7d 100644 --- a/graph/test/javascript/expected/69-vue-sfc.edges +++ b/graph/test/javascript/expected/69-vue-sfc.edges @@ -1,5 +1,5 @@ Comp.vue:10:34 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper -Comp.vue:10:6 FUNCTION_CALL Child -> known_edge child.js:1:1 Child -Comp.vue:11:6 FUNCTION_CALL MyBadge -> known_edge child.js:5:1 MyBadge +Comp.vue:10:6 JSX_ELEMENT Child(/**/) -> known_edge child.js:1:1 Child +Comp.vue:11:6 JSX_ELEMENT MyBadge(/**/) -> known_edge child.js:5:1 MyBadge Comp.vue:12:21 FUNCTION_CALL onSave -> known_edge Comp.vue:4:1 onSave Comp.vue:12:40 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag b/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag new file mode 100644 index 00000000..fe9810d3 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag @@ -0,0 +1,35 @@ +import_cause bundler-alias/legacy/app.js:4:10 Lib/extra not_staged +import_cause bundler-alias/legacy/app.js:5:10 Loose/extra not_staged +import_cause bundler-alias/legacy/webpack.config.cjs:1:14 path builtin +import_cause bundler-alias/src/pages/Home.js:6:10 @utils/date not_staged +import_cause bundler-alias/vite.config.js:1:10 vite not_staged +import_cause bundler-alias/vite.config.js:2:1 path builtin +import_cause bundler-alias/vite.config.js:3:10 node:url builtin +import_cause bundler-alias/vite.config.js:3:25 node:url builtin +import_cause framework-alias/other/probe.js:2:10 $lib/helper not_staged +import_cause jsconfig-paths/noalias/probe.js:2:10 @/check not_staged +import_cause workspace-package/apps/app/src/main.js:5:10 not-in-this-repo not_staged +package_entry @ws/app . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry @ws/lib . [] MAIN dist/index.js MISSING_FILE -> - +package_entry @ws/lib . [import] EXPORTS dist/index.mjs MISSING_FILE -> - +package_entry @ws/lib . [require] EXPORTS dist/index.js MISSING_FILE -> - +package_entry @ws/lib ./sub [] EXPORTS dist/sub/index.js MISSING_FILE -> - +package_entry bundler-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry path-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry ws-tools . [] MAIN dist/index.js MISSING_FILE -> - +unresolved bundler-alias/legacy/app.js:10:3 FUNCTION_CALL loose callee_untyped +unresolved bundler-alias/legacy/app.js:9:3 FUNCTION_CALL extra callee_untyped +unresolved bundler-alias/legacy/lib/util/trim.js:2:10 METHOD_CALL trim receiver_untyped +unresolved bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL resolve no_target +unresolved bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL join no_target +unresolved bundler-alias/shared/fmt.js:2:10 METHOD_CALL toFixed receiver_untyped +unresolved bundler-alias/src/pages/Home.js:10:3 FUNCTION_CALL scoped callee_untyped +unresolved bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String no_target +unresolved bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath no_target +unresolved bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL no_target +unresolved bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig callee_untyped +unresolved bundler-alias/vite.config.js:9:12 METHOD_CALL resolve receiver_untyped +unresolved framework-alias/other/probe.js:5:10 FUNCTION_CALL helper callee_untyped +unresolved jsconfig-paths/noalias/probe.js:5:10 FUNCTION_CALL checkA callee_untyped +unresolved workspace-package/apps/app/src/main.js:12:10 FUNCTION_CALL outside callee_untyped diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges b/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges new file mode 100644 index 00000000..6dc90ef1 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges @@ -0,0 +1,35 @@ +bundler-alias/legacy/app.js:10:3 FUNCTION_CALL loose -> ambiguous_unknown - +bundler-alias/legacy/app.js:11:10 FUNCTION_CALL trim -> known_edge bundler-alias/legacy/lib/util/trim.js:1:1 trim +bundler-alias/legacy/app.js:8:3 FUNCTION_CALL boot -> known_edge bundler-alias/legacy/lib/index.js:1:1 boot +bundler-alias/legacy/app.js:9:3 FUNCTION_CALL extra -> ambiguous_unknown - +bundler-alias/legacy/lib/util/trim.js:2:10 METHOD_CALL s.trim -> ambiguous_unknown - +bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL path.resolve -> ambient_terminal - +bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL path.join -> ambient_terminal - +bundler-alias/mapped/probe.js:5:10 FUNCTION_CALL fmtDate -> known_edge bundler-alias/mapped/own/date.js:1:1 fmtDate +bundler-alias/shared/fmt.js:2:10 METHOD_CALL n.toFixed -> ambiguous_unknown - +bundler-alias/src/pages/Home.js:10:3 FUNCTION_CALL scoped -> ambiguous_unknown - +bundler-alias/src/pages/Home.js:11:10 FUNCTION_CALL fmtDate -> known_edge bundler-alias/src/utils/date.js:1:1 fmtDate +bundler-alias/src/pages/Home.js:9:3 FUNCTION_CALL fmtMoney -> known_edge bundler-alias/shared/fmt.js:1:1 fmtMoney +bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String -> ambient_terminal - +bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath -> ambient_terminal - +bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL -> ambient_terminal - +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig -> ambiguous_unknown - +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig -> callback_registered bundler-alias/vite.config.js:6:29 +bundler-alias/vite.config.js:9:12 METHOD_CALL path.resolve -> ambient_terminal - +framework-alias/kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge framework-alias/kit/src/lib/api.js:5:1 post +framework-alias/kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge framework-alias/kit/src/lib/api.js:1:1 get +framework-alias/nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge framework-alias/nuxt/utils/price.js:1:1 formatPrice +framework-alias/nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge framework-alias/nuxt/utils/price.js:1:1 formatPrice +framework-alias/nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge framework-alias/nuxt4/app/utils/date.js:1:1 formatDate +framework-alias/other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - +framework-alias/own/probe.js:5:10 FUNCTION_CALL pick -> known_edge framework-alias/own/shared/util.js:1:1 pick +jsconfig-paths/app.js:11:10 FUNCTION_CALL checkB -> known_edge jsconfig-paths/lib/check.js:5:1 checkB +jsconfig-paths/app.js:7:10 FUNCTION_CALL checkA -> known_edge jsconfig-paths/lib/check.js:1:1 checkA +jsconfig-paths/noalias/probe.js:5:10 FUNCTION_CALL checkA -> ambiguous_unknown - +jsconfig-paths/remix/app/route.js:5:10 FUNCTION_CALL getNote -> known_edge jsconfig-paths/remix/app/models/note.server.js:1:1 getNote +workspace-package/apps/app/src/main.js:10:3 FUNCTION_CALL deep -> known_edge workspace-package/packages/tools/src/deep.js:1:1 deep +workspace-package/apps/app/src/main.js:11:3 CONSTRUCTOR_CALL Bus -> implicit_constructor - +workspace-package/apps/app/src/main.js:11:3 METHOD_CALL new Bus().publish -> known_edge workspace-package/packages/lib/src/index.js:6:3 publish +workspace-package/apps/app/src/main.js:12:10 FUNCTION_CALL outside -> ambiguous_unknown - +workspace-package/apps/app/src/main.js:8:3 FUNCTION_CALL f -> known_edge workspace-package/packages/lib/src/index.js:1:1 f +workspace-package/apps/app/src/main.js:9:3 FUNCTION_CALL g -> known_edge workspace-package/packages/lib/src/sub/index.js:1:1 g diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle b/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle new file mode 100644 index 00000000..c5403e62 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle @@ -0,0 +1,9 @@ +bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL resolve LIB_AMBIENT_OK +bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL join LIB_AMBIENT_OK +bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String LIB_AMBIENT_OK +bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath LIB_AMBIENT_OK +bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL LIB_AMBIENT_OK +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig LIB_MISSED +bundler-alias/vite.config.js:9:12 METHOD_CALL resolve LIB_AMBIENT_OK +jsconfig-paths/app.js:11:10 FUNCTION_CALL checkB EXACT jsconfig-paths/lib/check.js:5:1 +# defects: 0 diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.diag b/graph/test/javascript/expected/70-jsconfig-path-alias.diag deleted file mode 100644 index 74d4aa41..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.diag +++ /dev/null @@ -1,3 +0,0 @@ -import_cause noalias/probe.js:2:10 @/check not_staged -package_entry path-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - -unresolved noalias/probe.js:5:10 FUNCTION_CALL checkA callee_untyped diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.edges b/graph/test/javascript/expected/70-jsconfig-path-alias.edges deleted file mode 100644 index 09a8c77c..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.edges +++ /dev/null @@ -1,4 +0,0 @@ -app.js:11:10 FUNCTION_CALL checkB -> known_edge lib/check.js:5:1 checkB -app.js:7:10 FUNCTION_CALL checkA -> known_edge lib/check.js:1:1 checkA -noalias/probe.js:5:10 FUNCTION_CALL checkA -> ambiguous_unknown - -remix/app/route.js:5:10 FUNCTION_CALL getNote -> known_edge remix/app/models/note.server.js:1:1 getNote diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.oracle b/graph/test/javascript/expected/70-jsconfig-path-alias.oracle deleted file mode 100644 index 16df3e53..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.oracle +++ /dev/null @@ -1,2 +0,0 @@ -app.js:11:10 FUNCTION_CALL checkB EXACT lib/check.js:5:1 -# defects: 0 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/70-single-file-component-scripts.edges b/graph/test/javascript/expected/70-single-file-component-scripts.edges index 0bb4c4cb..21bf8d41 100644 --- a/graph/test/javascript/expected/70-single-file-component-scripts.edges +++ b/graph/test/javascript/expected/70-single-file-component-scripts.edges @@ -7,6 +7,7 @@ components/List.svelte:5:39 FUNCTION_CALL fetchProducts -> known_edge lib/for components/List.svelte:6:29 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice components/Price.vue:1:25 FUNCTION_CALL onClick -> known_edge components/Price.vue:8:1 onClick components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> ambiguous_unknown - +components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> callback_registered components/Price.vue:7:24 components/Price.vue:6:15 FUNCTION_CALL defineProps -> ambiguous_unknown - components/Price.vue:7:15 FUNCTION_CALL computed -> ambiguous_unknown - components/Price.vue:7:15 FUNCTION_CALL computed -> callback_registered components/Price.vue:7:24 diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.diag b/graph/test/javascript/expected/71-framework-generated-config-alias.diag deleted file mode 100644 index 008e2a73..00000000 --- a/graph/test/javascript/expected/71-framework-generated-config-alias.diag +++ /dev/null @@ -1,3 +0,0 @@ -import_cause other/probe.js:2:10 $lib/helper not_staged -package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - -unresolved other/probe.js:5:10 FUNCTION_CALL helper callee_untyped diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.edges b/graph/test/javascript/expected/71-framework-generated-config-alias.edges deleted file mode 100644 index 916bc000..00000000 --- a/graph/test/javascript/expected/71-framework-generated-config-alias.edges +++ /dev/null @@ -1,7 +0,0 @@ -kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge kit/src/lib/api.js:5:1 post -kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge kit/src/lib/api.js:1:1 get -nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge nuxt/utils/price.js:1:1 formatPrice -nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge nuxt/utils/price.js:1:1 formatPrice -nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge nuxt4/app/utils/date.js:1:1 formatDate -other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - -own/probe.js:5:10 FUNCTION_CALL pick -> known_edge own/shared/util.js:1:1 pick diff --git a/graph/test/javascript/expected/71-jsx-vue-define-component.diag b/graph/test/javascript/expected/71-jsx-vue-define-component.diag new file mode 100644 index 00000000..31c98cac --- /dev/null +++ b/graph/test/javascript/expected/71-jsx-vue-define-component.diag @@ -0,0 +1,10 @@ +import_cause app.jsx:1:10 vue not_staged +import_cause app.jsx:2:10 store-kit not_staged +import_cause def.jsx:1:10 vue not_staged +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved app.jsx:12:25 FUNCTION_CALL defineComponent callee_untyped +unresolved app.jsx:14:26 FUNCTION_CALL defineComponent callee_untyped +unresolved app.jsx:16:22 FUNCTION_CALL defineComponent callee_untyped +unresolved app.jsx:18:24 FUNCTION_CALL defineComponent callee_untyped +unresolved app.jsx:21:22 FUNCTION_CALL makeStore callee_untyped +unresolved def.jsx:6:16 FUNCTION_CALL defineComponent callee_untyped diff --git a/graph/test/javascript/expected/71-jsx-vue-define-component.edges b/graph/test/javascript/expected/71-jsx-vue-define-component.edges new file mode 100644 index 00000000..e26d823f --- /dev/null +++ b/graph/test/javascript/expected/71-jsx-vue-define-component.edges @@ -0,0 +1,25 @@ +app.jsx:12:25 FUNCTION_CALL defineComponent -> ambiguous_unknown - +app.jsx:12:25 FUNCTION_CALL defineComponent -> callback_registered app.jsx:12:43 setup +app.jsx:12:53 FUNCTION_CALL leaf -> known_edge app.jsx:5:1 leaf +app.jsx:14:26 FUNCTION_CALL defineComponent -> ambiguous_unknown - +app.jsx:14:26 FUNCTION_CALL defineComponent -> callback_registered app.jsx:14:44 render +app.jsx:14:55 FUNCTION_CALL renderLeaf -> known_edge app.jsx:6:1 renderLeaf +app.jsx:16:22 FUNCTION_CALL defineComponent -> ambiguous_unknown - +app.jsx:16:22 FUNCTION_CALL defineComponent -> callback_registered app.jsx:16:38 +app.jsx:16:51 FUNCTION_CALL fnLeaf -> known_edge app.jsx:7:1 fnLeaf +app.jsx:18:24 FUNCTION_CALL defineComponent -> ambiguous_unknown - +app.jsx:18:24 FUNCTION_CALL defineComponent -> callback_registered app.jsx:18:42 setup +app.jsx:18:24 FUNCTION_CALL defineComponent -> callback_registered app.jsx:18:85 render +app.jsx:18:52 FUNCTION_CALL bothLeaf -> known_edge app.jsx:8:1 bothLeaf +app.jsx:21:22 FUNCTION_CALL makeStore -> ambiguous_unknown - +app.jsx:21:22 FUNCTION_CALL makeStore -> callback_registered app.jsx:21:34 setup +app.jsx:21:44 FUNCTION_CALL storeLeaf -> known_edge app.jsx:9:1 storeLeaf +app.jsx:26:7 JSX_ELEMENT -> known_edge app.jsx:12:43 setup +app.jsx:27:7 JSX_ELEMENT -> known_edge app.jsx:14:44 render +app.jsx:28:7 JSX_ELEMENT -> known_edge app.jsx:16:38 +app.jsx:29:7 JSX_ELEMENT -> known_edge app.jsx:18:42 setup +app.jsx:30:7 JSX_ELEMENT -> known_edge def.jsx:6:34 setup +app.jsx:31:7 JSX_ELEMENT -> ambiguous_unknown - +def.jsx:6:16 FUNCTION_CALL defineComponent -> ambiguous_unknown - +def.jsx:6:16 FUNCTION_CALL defineComponent -> callback_registered def.jsx:6:34 setup +def.jsx:6:44 FUNCTION_CALL defLeaf -> known_edge def.jsx:3:1 defLeaf diff --git a/graph/test/javascript/expected/71-jsx-vue-define-component.oracle b/graph/test/javascript/expected/71-jsx-vue-define-component.oracle new file mode 100644 index 00000000..2bfd938e --- /dev/null +++ b/graph/test/javascript/expected/71-jsx-vue-define-component.oracle @@ -0,0 +1,7 @@ +app.jsx:12:53 FUNCTION_CALL leaf EXACT app.jsx:5:1 +app.jsx:14:55 FUNCTION_CALL renderLeaf EXACT app.jsx:6:1 +app.jsx:16:51 FUNCTION_CALL fnLeaf EXACT app.jsx:7:1 +app.jsx:18:52 FUNCTION_CALL bothLeaf EXACT app.jsx:8:1 +app.jsx:21:44 FUNCTION_CALL storeLeaf EXACT app.jsx:9:1 +def.jsx:6:44 FUNCTION_CALL defLeaf EXACT def.jsx:3:1 +# defects: 0 diff --git a/graph/test/javascript/expected/72-jsx-context-connect-props.diag b/graph/test/javascript/expected/72-jsx-context-connect-props.diag new file mode 100644 index 00000000..f3db0ab8 --- /dev/null +++ b/graph/test/javascript/expected/72-jsx-context-connect-props.diag @@ -0,0 +1,26 @@ +import_cause Auth.jsx:1:10 react not_staged +import_cause Auth.jsx:1:25 react not_staged +import_cause Auth.jsx:1:38 react not_staged +import_cause Auth.jsx:1:50 react not_staged +import_cause Profile.jsx:1:1 react not_staged +import_cause Profile.jsx:2:10 react-redux not_staged +import_cause Settings.jsx:1:10 react-redux not_staged +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved Auth.jsx:12:18 FUNCTION_CALL useCallback callee_untyped +unresolved Auth.jsx:13:27 FUNCTION_CALL useMemo callee_untyped +unresolved Auth.jsx:18:22 FUNCTION_CALL useContext callee_untyped +unresolved Auth.jsx:23:30 FUNCTION_CALL useContext callee_untyped +unresolved Auth.jsx:32:30 FUNCTION_CALL useContext callee_untyped +unresolved Auth.jsx:3:24 FUNCTION_CALL createContext callee_untyped +unresolved Auth.jsx:4:25 FUNCTION_CALL createContext callee_untyped +unresolved Profile.jsx:14:16 FUNCTION_CALL callee_untyped +unresolved Profile.jsx:14:16 FUNCTION_CALL connect callee_untyped +unresolved Profile.jsx:25:35 FUNCTION_CALL callee_untyped +unresolved Profile.jsx:25:35 FUNCTION_CALL connect callee_untyped +unresolved Settings.jsx:12:16 FUNCTION_CALL callee_untyped +unresolved Settings.jsx:12:16 FUNCTION_CALL connect callee_untyped +unresolved Settings.jsx:9:24 FUNCTION_CALL dispatch callee_untyped +value_callee Profile.jsx:14:16 connect(null, { onSave: saveProfile }) expression +value_callee Profile.jsx:25:35 connect(null, { onOpen: openFavorites }) expression +value_callee Settings.jsx:12:16 connect(mapStateToProps, mapDispatchToProps) expression +value_callee Settings.jsx:9:24 dispatch parameter diff --git a/graph/test/javascript/expected/72-jsx-context-connect-props.edges b/graph/test/javascript/expected/72-jsx-context-connect-props.edges new file mode 100644 index 00000000..a9b882a8 --- /dev/null +++ b/graph/test/javascript/expected/72-jsx-context-connect-props.edges @@ -0,0 +1,45 @@ +Auth.jsx:12:18 FUNCTION_CALL useCallback -> ambiguous_unknown - +Auth.jsx:12:18 FUNCTION_CALL useCallback -> callback_registered Auth.jsx:12:30 +Auth.jsx:13:10 JSX_ELEMENT ({ toggle }), [toggle])}>{chi -> ambiguous_unknown - +Auth.jsx:13:27 FUNCTION_CALL useMemo -> ambiguous_unknown - +Auth.jsx:13:27 FUNCTION_CALL useMemo -> callback_registered Auth.jsx:13:35 +Auth.jsx:18:22 FUNCTION_CALL useContext -> ambiguous_unknown - +Auth.jsx:19:18 JSX_ATTRIBUTE onClick={logout} -> callback_registered Auth.jsx:7:18 +Auth.jsx:23:30 FUNCTION_CALL useContext -> ambiguous_unknown - +Auth.jsx:27:10 JSX_ELEMENT {(auth) => out ambiguous_unknown - +Auth.jsx:27:42 JSX_ATTRIBUTE onClick={auth.logout} -> callback_registered Auth.jsx:7:18 +Auth.jsx:32:30 FUNCTION_CALL useContext -> ambiguous_unknown - +Auth.jsx:33:18 JSX_ATTRIBUTE onClick={toggle} -> callback_registered Auth.jsx:12:30 +Auth.jsx:33:18 JSX_ATTRIBUTE onClick={toggle} -> callback_registered Auth.jsx:4:49 +Auth.jsx:3:24 FUNCTION_CALL createContext -> ambiguous_unknown - +Auth.jsx:4:25 FUNCTION_CALL createContext -> ambiguous_unknown - +Auth.jsx:4:25 FUNCTION_CALL createContext -> callback_registered Auth.jsx:4:49 +Auth.jsx:8:10 JSX_ELEMENT {children} ambiguous_unknown - +Menu.jsx:5:16 FUNCTION_CALL useAuth -> known_edge Auth.jsx:23:24 +Menu.jsx:6:14 JSX_ATTRIBUTE onClick={auth.logout} -> callback_registered Auth.jsx:7:18 +Plain.jsx:9:21 FUNCTION_CALL connect -> known_edge db.js:1:1 connect +Profile.jsx:10:18 JSX_ATTRIBUTE onSubmit={onSave} -> callback_registered Profile.jsx:4:1 saveProfile +Profile.jsx:10:44 JSX_ATTRIBUTE onClick={this.props.onSave} -> callback_registered Profile.jsx:4:1 saveProfile +Profile.jsx:14:16 FUNCTION_CALL connect -> ambiguous_unknown - +Profile.jsx:14:16 FUNCTION_CALL connect -> callback_registered Profile.jsx:4:1 saveProfile +Profile.jsx:14:16 FUNCTION_CALL connect(null, { onSave: saveProfile }) -> ambiguous_unknown - +Profile.jsx:21:15 JSX_ATTRIBUTE onClick={this.props.onOpen} -> callback_registered Profile.jsx:16:1 openFavorites +Profile.jsx:25:35 FUNCTION_CALL connect -> ambiguous_unknown - +Profile.jsx:25:35 FUNCTION_CALL connect -> callback_registered Profile.jsx:16:1 openFavorites +Profile.jsx:25:35 FUNCTION_CALL connect(null, { onOpen: openFavorites }) -> ambiguous_unknown - +Settings.jsx:12:16 FUNCTION_CALL connect -> ambiguous_unknown - +Settings.jsx:12:16 FUNCTION_CALL connect -> callback_registered Settings.jsx:7:25 +Settings.jsx:12:16 FUNCTION_CALL connect -> callback_registered Settings.jsx:8:28 +Settings.jsx:12:16 FUNCTION_CALL connect(mapStateToProps, mapDispatchToProps) -> ambiguous_unknown - +Settings.jsx:12:16 FUNCTION_CALL connect(mapStateToProps, mapDispatchToProps) -> callback_registered Settings.jsx:3:1 Settings +Settings.jsx:4:18 JSX_ATTRIBUTE onClick={props.onClickLogout} -> callback_registered Settings.jsx:9:18 +Settings.jsx:9:24 FUNCTION_CALL dispatch -> ambiguous_unknown - +main.jsx:10:7 JSX_ELEMENT \n known_edge Auth.jsx:11:1 ThemeProvider +main.jsx:11:25 JSX_ELEMENT -> known_edge Auth.jsx:26:1 LogoutLink +main.jsx:11:39 JSX_ELEMENT -> known_edge Auth.jsx:31:1 ThemeButton +main.jsx:11:54 JSX_ELEMENT -> known_edge Menu.jsx:4:1 Menu +main.jsx:11:9 JSX_ELEMENT -> known_edge Auth.jsx:17:1 LogoutButton +main.jsx:12:30 JSX_ELEMENT -> known_edge Profile.jsx:8:3 render +main.jsx:12:50 JSX_ELEMENT -> known_edge Plain.jsx:4:1 Plain +main.jsx:12:9 JSX_ELEMENT -> known_edge Settings.jsx:3:1 Settings +main.jsx:9:5 JSX_ELEMENT \n \n known_edge Auth.jsx:6:1 AuthProvider diff --git a/graph/test/javascript/expected/72-jsx-context-connect-props.oracle b/graph/test/javascript/expected/72-jsx-context-connect-props.oracle new file mode 100644 index 00000000..b3a2dc1f --- /dev/null +++ b/graph/test/javascript/expected/72-jsx-context-connect-props.oracle @@ -0,0 +1,3 @@ +Menu.jsx:5:16 FUNCTION_CALL useAuth EXACT Auth.jsx:23:24 +Plain.jsx:9:21 FUNCTION_CALL connect EXACT db.js:1:1 +# defects: 0 diff --git a/graph/test/javascript/expected/72-vue-component-tag.diag b/graph/test/javascript/expected/72-vue-component-tag.diag new file mode 100644 index 00000000..ef755ad8 --- /dev/null +++ b/graph/test/javascript/expected/72-vue-component-tag.diag @@ -0,0 +1,12 @@ +import_cause components/Defined.vue:2:10 vue not_staged +import_cause views/Parent.vue:8:10 vue not_staged +import_cause views/Shell.vue:3:1 @/Gone.vue not_staged +import_cause vite.config.js:1:10 node:url builtin +import_cause vite.config.js:1:25 node:url builtin +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved components/Defined.vue:3:16 FUNCTION_CALL defineComponent callee_untyped +unresolved components/MyWidget.vue:2:1 FUNCTION_CALL defineProps callee_untyped +unresolved views/Parent.vue:11:26 FUNCTION_CALL String no_target +unresolved views/Parent.vue:9:15 FUNCTION_CALL defineComponent callee_untyped +unresolved vite.config.js:3:28 FUNCTION_CALL fileURLToPath no_target +unresolved vite.config.js:3:42 CONSTRUCTOR_CALL URL no_target diff --git a/graph/test/javascript/expected/72-vue-component-tag.edges b/graph/test/javascript/expected/72-vue-component-tag.edges new file mode 100644 index 00000000..91c94a20 --- /dev/null +++ b/graph/test/javascript/expected/72-vue-component-tag.edges @@ -0,0 +1,26 @@ +components/Aliased.vue:4:19 FUNCTION_CALL aliasedLocal -> known_edge components/Aliased.vue:2:1 aliasedLocal +components/Child.vue:4:18 FUNCTION_CALL childLocal -> known_edge components/Child.vue:2:1 childLocal +components/Defined.vue:3:16 FUNCTION_CALL defineComponent -> ambiguous_unknown - +components/Defined.vue:3:16 FUNCTION_CALL defineComponent -> callback_registered components/Defined.vue:4:3 setup +components/Defined.vue:4:20 FUNCTION_CALL definedSetup -> known_edge components/Defined.vue:6:1 definedSetup +components/Legacy.vue:4:78 FUNCTION_CALL legacyInit -> known_edge components/Legacy.vue:3:1 legacyInit +components/Legacy.vue:6:15 JSX_ELEMENT Child(/**/) -> known_edge components/Child.vue:1:1 +components/MyWidget.vue:2:1 FUNCTION_CALL defineProps -> ambiguous_unknown - +components/MyWidget.vue:5:21 FUNCTION_CALL widgetLabel -> known_edge components/MyWidget.vue:3:1 widgetLabel +views/Parent.vue:11:26 FUNCTION_CALL String -> ambient_terminal - +views/Parent.vue:15:6 JSX_ELEMENT Child(/**/) -> known_edge components/Child.vue:1:1 +views/Parent.vue:16:6 JSX_ELEMENT MyWidget(/**/) -> known_edge components/MyWidget.vue:1:1 +views/Parent.vue:17:6 JSX_ELEMENT Legacy(/**/) -> known_edge components/Legacy.vue:1:1 +views/Parent.vue:18:6 JSX_ELEMENT Defined(/**/) -> known_edge components/Defined.vue:4:3 setup +views/Parent.vue:19:6 JSX_ELEMENT Local(/**/) -> known_edge views/Parent.vue:9:33 setup +views/Parent.vue:20:6 JSX_ELEMENT Badge(/**/) -> known_edge components/badge.js:1:1 Badge +views/Parent.vue:21:6 JSX_ELEMENT Store(/**/) -> ambiguous_unknown - +views/Parent.vue:22:15 FUNCTION_CALL fmt -> known_edge views/Parent.vue:11:1 fmt +views/Parent.vue:9:15 FUNCTION_CALL defineComponent -> ambiguous_unknown - +views/Parent.vue:9:15 FUNCTION_CALL defineComponent -> callback_registered views/Parent.vue:9:33 setup +views/Parent.vue:9:50 FUNCTION_CALL localSetup -> known_edge views/Parent.vue:10:1 localSetup +views/Shell.vue:10:6 JSX_ELEMENT Badge(/**/) -> known_edge components/badge.js:1:1 Badge +views/Shell.vue:8:6 JSX_ELEMENT Aliased(/**/) -> known_edge components/Aliased.vue:1:1 +views/Shell.vue:9:6 JSX_ELEMENT Gone(/**/) -> ambiguous_unknown - +vite.config.js:3:28 FUNCTION_CALL fileURLToPath -> ambient_terminal - +vite.config.js:3:42 CONSTRUCTOR_CALL URL -> ambient_terminal - diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.oracle b/graph/test/javascript/expected/72-vue-component-tag.oracle similarity index 100% rename from graph/test/javascript/expected/71-framework-generated-config-alias.oracle rename to graph/test/javascript/expected/72-vue-component-tag.oracle diff --git a/graph/test/javascript/expected/73-bound-function-identity.diag b/graph/test/javascript/expected/73-bound-function-identity.diag new file mode 100644 index 00000000..fcdd22f9 --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.diag @@ -0,0 +1,11 @@ +import_cause bound.js:28:14 util builtin +import_cause bound.js:2:9 util builtin +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved bound.js:17:19 FUNCTION_CALL promisify no_target +unresolved bound.js:26:17 FUNCTION_CALL promisify no_target +unresolved bound.js:29:22 METHOD_CALL promisify no_target +unresolved bound.js:42:30 METHOD_CALL append receiver_untyped +unresolved bound.js:45:35 FUNCTION_CALL_BIND fn callee_untyped +unresolved bound.js:45:57 FUNCTION_CALL g callee_untyped +value_callee bound.js:45:35 fn.bind parameter +value_callee bound.js:45:57 g local diff --git a/graph/test/javascript/expected/73-bound-function-identity.edges b/graph/test/javascript/expected/73-bound-function-identity.edges new file mode 100644 index 00000000..6cab9951 --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.edges @@ -0,0 +1,35 @@ +bound.js:16:16 FUNCTION_CALL_BIND this.repo.append.bind -> known_edge bound.js:5:3 append +bound.js:17:19 FUNCTION_CALL promisify -> ambient_terminal - +bound.js:17:19 FUNCTION_CALL promisify -> callback_registered bound.js:6:3 find +bound.js:17:29 FUNCTION_CALL_BIND r.find.bind -> known_edge bound.js:6:3 find +bound.js:19:17 METHOD_CALL this.add -> known_edge bound.js:5:3 append +bound.js:20:19 METHOD_CALL this.lookup -> known_edge bound.js:6:3 find +bound.js:22:33 CONSTRUCTOR_CALL Service -> known_edge bound.js:14:3 +bound.js:22:45 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:23:14 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:24:15 FUNCTION_CALL_BIND repo.append.bind -> known_edge bound.js:5:3 append +bound.js:25:33 FUNCTION_CALL bound -> known_edge bound.js:5:3 append +bound.js:26:17 FUNCTION_CALL promisify -> ambient_terminal - +bound.js:26:17 FUNCTION_CALL promisify -> callback_registered bound.js:6:3 find +bound.js:26:27 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:27:36 FUNCTION_CALL wrapped -> known_edge bound.js:6:3 find +bound.js:29:22 METHOD_CALL util.promisify -> ambient_terminal - +bound.js:29:22 METHOD_CALL util.promisify -> callback_registered bound.js:6:3 find +bound.js:29:37 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:30:38 FUNCTION_CALL viaNamespace -> known_edge bound.js:6:3 find +bound.js:33:30 METHOD_CALL this.repo.append -> known_edge bound.js:5:3 append +bound.js:34:17 FUNCTION_CALL_BIND onEvent.bind -> known_edge bound.js:33:1 onEvent +bound.js:34:38 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:35:26 FUNCTION_CALL handler -> known_edge bound.js:33:1 onEvent +bound.js:37:43 METHOD_CALL this.store.append -> known_edge bound.js:9:3 append +bound.js:39:24 FUNCTION_CALL_BIND handlers.created.bind -> known_edge bound.js:37:12 onCreated +bound.js:39:55 CONSTRUCTOR_CALL Other -> implicit_constructor - +bound.js:42:30 METHOD_CALL this.repo.append -> ambiguous_unknown - +bound.js:43:34 FUNCTION_CALL_CALL unbound.call -> known_edge bound.js:42:1 unbound +bound.js:43:55 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:44:18 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:45:35 FUNCTION_CALL_BIND fn.bind -> ambiguous_unknown - +bound.js:45:57 FUNCTION_CALL g -> ambiguous_unknown - +bound.js:47:17 METHOD_CALL own.promisify -> callback_registered bound.js:6:3 find +bound.js:47:17 METHOD_CALL own.promisify -> known_edge bound.js:46:15 promisify +bound.js:48:37 FUNCTION_CALL notUtil -> known_edge bound.js:46:37 diff --git a/graph/test/javascript/expected/73-bound-function-identity.oracle b/graph/test/javascript/expected/73-bound-function-identity.oracle new file mode 100644 index 00000000..01c9e34b --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.oracle @@ -0,0 +1,15 @@ +bound.js:22:33 CONSTRUCTOR_CALL Service EXACT bound.js:14:3 +bound.js:22:45 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:23:14 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:24:15 FUNCTION_CALL_BIND bind EXACT bound.js:5:3 +bound.js:26:27 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:29:37 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:34:17 FUNCTION_CALL_BIND bind EXACT bound.js:33:1 +bound.js:34:38 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:39:24 FUNCTION_CALL_BIND bind EXACT bound.js:37:12 +bound.js:39:55 CONSTRUCTOR_CALL Other SYNTHESIZED_OK +bound.js:43:34 FUNCTION_CALL_CALL call EXACT bound.js:42:1 +bound.js:43:55 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:44:18 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:47:17 METHOD_CALL promisify EXACT bound.js:46:15 +# defects: 0 diff --git a/graph/test/javascript/expected/74-orm-model-factory.diag b/graph/test/javascript/expected/74-orm-model-factory.diag new file mode 100644 index 00000000..f55347e0 --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.diag @@ -0,0 +1,21 @@ +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 +unresolved models.js:12:1 METHOD_CALL static receiver_untyped +unresolved models.js:13:1 METHOD_CALL static receiver_untyped +unresolved models.js:15:1 METHOD_CALL method receiver_untyped +unresolved models.js:18:19 CONSTRUCTOR_CALL Schema receiver_untyped +unresolved models.js:22:21 CONSTRUCTOR_CALL OtherSchema callee_untyped +unresolved models.js:27:35 METHOD_CALL model receiver_untyped +unresolved models.js:28:10 METHOD_CALL model receiver_untyped +unresolved models.js:29:12 METHOD_CALL model receiver_untyped +unresolved repo.js:10:21 CONSTRUCTOR_CALL Doc member_absent +unresolved repo.js:11:19 METHOD_CALL create receiver_untyped +unresolved repo.js:12:20 METHOD_CALL findLive receiver_untyped +value_callee models.js:10:19 Schema module_variable diff --git a/graph/test/javascript/expected/74-orm-model-factory.edges b/graph/test/javascript/expected/74-orm-model-factory.edges new file mode 100644 index 00000000..f6b6bf7a --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.edges @@ -0,0 +1,36 @@ +models.js:10:19 CONSTRUCTOR_CALL Schema -> ambiguous_unknown - +models.js:11:51 METHOD_CALL this.find -> ambiguous_unknown - +models.js:12:1 METHOD_CALL docSchema.static -> ambiguous_unknown - +models.js:12:1 METHOD_CALL docSchema.static -> callback_registered models.js:12:27 +models.js:12:48 METHOD_CALL this.findLive -> known_edge models.js:11:30 +models.js:13:1 METHOD_CALL docSchema.static -> ambiguous_unknown - +models.js:13:1 METHOD_CALL docSchema.static -> callback_registered models.js:13:20 purge +models.js:15:1 METHOD_CALL docSchema.method -> ambiguous_unknown - +models.js:15:1 METHOD_CALL docSchema.method -> callback_registered models.js:15:27 +models.js:15:48 METHOD_CALL this.toRecord -> known_edge models.js:14:30 +models.js:18:19 CONSTRUCTOR_CALL mongoose.Schema -> ambiguous_unknown - +models.js:18:19 CONSTRUCTOR_CALL mongoose.Schema -> callback_registered models.js:18:56 due +models.js:22:21 CONSTRUCTOR_CALL OtherSchema -> ambiguous_unknown - +models.js:27:35 METHOD_CALL connection.model -> ambiguous_unknown - +models.js:28:10 METHOD_CALL mongoose.model -> ambiguous_unknown - +models.js:29:12 METHOD_CALL connection.model -> ambiguous_unknown - +repo.js:10:21 CONSTRUCTOR_CALL this.Doc -> ambiguous_unknown - +repo.js:10:21 METHOD_CALL new this.Doc({}).touch -> known_edge models.js:15:27 +repo.js:11:19 METHOD_CALL this.Doc.create -> ambiguous_unknown - +repo.js:12:20 METHOD_CALL this.Other.findLive -> ambiguous_unknown - +repo.js:24:15 METHOD_CALL registry.model -> known_edge repo.js:23:20 model +repo.js:27:16 CONSTRUCTOR_CALL DocRepository -> known_edge repo.js:5:3 +repo.js:27:34 FUNCTION_CALL registerModels -> known_edge models.js:25:1 registerModels +repo.js:28:11 METHOD_CALL repo.list -> known_edge repo.js:6:3 list +repo.js:28:24 METHOD_CALL repo.take -> known_edge repo.js:7:3 take +repo.js:28:37 METHOD_CALL repo.purge -> known_edge repo.js:8:3 purge +repo.js:28:51 METHOD_CALL repo.due -> known_edge repo.js:9:3 due +repo.js:28:63 METHOD_CALL repo.record -> known_edge repo.js:10:3 record +repo.js:28:78 METHOD_CALL repo.save -> known_edge repo.js:11:3 save +repo.js:28:91 METHOD_CALL repo.other -> known_edge repo.js:12:3 other +repo.js:29:23 CONSTRUCTOR_CALL MemoryRepository -> implicit_constructor - +repo.js:29:5 METHOD_CALL plain.findLive -> known_edge repo.js:24:37 findLive +repo.js:6:19 METHOD_CALL this.Doc.findLive -> known_edge models.js:11:30 +repo.js:7:19 METHOD_CALL this.Doc.claim -> known_edge models.js:12:27 +repo.js:8:20 METHOD_CALL this.Doc.purge -> known_edge models.js:13:20 purge +repo.js:9:18 METHOD_CALL this.Job.due -> known_edge models.js:18:56 due diff --git a/graph/test/javascript/expected/74-orm-model-factory.oracle b/graph/test/javascript/expected/74-orm-model-factory.oracle new file mode 100644 index 00000000..914865e5 --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.oracle @@ -0,0 +1,12 @@ +repo.js:24:15 METHOD_CALL model EXACT repo.js:23:20 +repo.js:27:16 CONSTRUCTOR_CALL DocRepository EXACT repo.js:5:3 +repo.js:27:34 FUNCTION_CALL registerModels EXACT models.js:25:1 +repo.js:28:11 METHOD_CALL list EXACT repo.js:6:3 +repo.js:28:24 METHOD_CALL take EXACT repo.js:7:3 +repo.js:28:37 METHOD_CALL purge EXACT repo.js:8:3 +repo.js:28:51 METHOD_CALL due EXACT repo.js:9:3 +repo.js:28:63 METHOD_CALL record EXACT repo.js:10:3 +repo.js:28:78 METHOD_CALL save EXACT repo.js:11:3 +repo.js:28:91 METHOD_CALL other EXACT repo.js:12:3 +repo.js:29:23 CONSTRUCTOR_CALL MemoryRepository SYNTHESIZED_OK +# defects: 0 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/08-callables/src/main.py b/graph/test/python/cases/08-callables/src/main.py index 80ac9186..baa116aa 100644 --- a/graph/test/python/cases/08-callables/src/main.py +++ b/graph/test/python/cases/08-callables/src/main.py @@ -9,7 +9,8 @@ partial functools.partial exposes `.func`, so CPython can name the target lambda stored in a dict, reached by subscript: no name at the call site attribute a plain function assigned to an instance attribute, which does NOT - go through the descriptor protocol and so is not a bound method + go through the descriptor protocol and so is not a bound method; + a partial stored there calls its target the same way """ import functools @@ -31,10 +32,16 @@ class Holder: def __init__(self): # A function on an INSTANCE attribute. Not a method: no `self` is bound. self.op = add + self.bump = functools.partial(add, 1) + # control: a partial over a builtin stays the platform's + self.biggest = functools.partial(max, 0) def use(self): return self.op(1, 2) + def use_partial(self): + return self.bump(2) + self.biggest(3) + TABLE = { # A lambda reached by subscript. There is no name to resolve. @@ -52,6 +59,7 @@ def main(): print(TABLE["double"](21)) print(Holder().use()) + print(Holder().use_partial()) if __name__ == "__main__": diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/tasks.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/tasks.py index 4adaa970..50f3118a 100644 --- a/graph/test/python/cases/29-framework-edge-consumers/src/app/tasks.py +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/tasks.py @@ -1,7 +1,7 @@ """Two registered tasks (one with bind=True), an unregistered function, and a class whose method is also called `delay`.""" -from celery import shared_task +from taskqueue import shared_task @shared_task diff --git a/graph/test/python/cases/35-class-attribute-instances/src/main.py b/graph/test/python/cases/35-class-attribute-instances/src/main.py new file mode 100644 index 00000000..10d621c4 --- /dev/null +++ b/graph/test/python/cases/35-class-attribute-instances/src/main.py @@ -0,0 +1,6 @@ +from shop.models import Order, list_via_class + + +def run(): + list_via_class() + return Order.objects.open_orders() diff --git a/graph/test/python/cases/35-class-attribute-instances/src/shop/__init__.py b/graph/test/python/cases/35-class-attribute-instances/src/shop/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/35-class-attribute-instances/src/shop/models.py b/graph/test/python/cases/35-class-attribute-instances/src/shop/models.py new file mode 100644 index 00000000..605908b2 --- /dev/null +++ b/graph/test/python/cases/35-class-attribute-instances/src/shop/models.py @@ -0,0 +1,58 @@ +"""35 -- a class attribute holding an instance, reached on the CLASS (#1518). + +INTENT: `objects = OrderManager()` in a class body puts one OrderManager on the +class object, so `Order.objects.open_orders()` calls OrderManager.open_orders +exactly as `Order().objects.open_orders()` does. The instance spelling was typed +and the class spelling was not. + +Controls: + audit written through `self` in __init__: it exists on instances only, + so `Order.audit` names nothing the class holds and stays untyped + SpecialOrder inherits `objects`, so the class spelling on the subclass reaches + the same manager through the MRO + kind a class-body assignment of a string: no construction, no type +""" + + +class OrderManager: + def open_orders(self): + return [] + + +class AuditLog: + def entries(self): + return [] + + +class Order: + objects = OrderManager() + kind = "order" + + def __init__(self): + self.audit = AuditLog() + + +class SpecialOrder(Order): + pass + + +def list_via_class(): + return Order.objects.open_orders() + + +def list_via_instance(): + return Order().objects.open_orders() + + +def list_via_subclass(): + return SpecialOrder.objects.open_orders() + + +def audit_via_class(): + # Near miss: an instance-only attribute read on the class. + return Order.audit.entries() + + +def kind_via_class(): + # Near miss: a class-body value that is not a construction. + return Order.kind.upper() diff --git a/graph/test/python/cases/36-call-through-model-decorators/src/app/__init__.py b/graph/test/python/cases/36-call-through-model-decorators/src/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/36-call-through-model-decorators/src/app/checkout.py b/graph/test/python/cases/36-call-through-model-decorators/src/app/checkout.py new file mode 100644 index 00000000..82252183 --- /dev/null +++ b/graph/test/python/cases/36-call-through-model-decorators/src/app/checkout.py @@ -0,0 +1,21 @@ +from app.orders import Order, apply_coupon, apply_discount, apply_tax, replaced + + +def checkout(price: float): + return apply_discount(price) + + +def checkout_coupon(price: float): + return apply_coupon(price) + + +def checkout_taxed(price: float): + return apply_tax(price) + + +def checkout_replaced(price: float): + return replaced(price) + + +def show(order: Order): + return order.grand_total.hex(), order.tax_due, order.net_total diff --git a/graph/test/python/cases/36-call-through-model-decorators/src/app/orders.py b/graph/test/python/cases/36-call-through-model-decorators/src/app/orders.py new file mode 100644 index 00000000..388b9a84 --- /dev/null +++ b/graph/test/python/cases/36-call-through-model-decorators/src/app/orders.py @@ -0,0 +1,61 @@ +"""36 -- decorators that validate or mark, then call the decorated def (#1534). + +INTENT: pydantic's `@validate_call` validates the arguments and calls the +function; `@computed_field` marks a property for serialization and a read still +runs the getter. Both call through, like `@functools.cache`, so the name at the +call site still reaches the `def`. As unknown decorators they were assumed to +replace their target and the def left lookup: the caller fell to by-name and the +computed field had no reader. + +Controls: + apply_tax undecorated: resolved before and after + net_total plain @property: resolved before and after + replaced an unknown decorator still replaces its target, so its caller + stays unresolved +""" +from pydantic import BaseModel, computed_field, validate_call + +import pydantic + + +def audit(fn): + def wrapper(*args, **kwargs): + return fn(*args, **kwargs) + + return wrapper + + +@validate_call +def apply_discount(price: float) -> float: + return round(price * 0.9, 2) + + +@pydantic.validate_call(validate_return=True) +def apply_coupon(price: float) -> float: + return price - 1 + + +def apply_tax(price: float) -> float: + return round(price * 1.2, 2) + + +@audit +def replaced(price: float) -> float: + return price + + +class Order(BaseModel): + total: float + + @computed_field + @property + def grand_total(self) -> float: + return self.total * 1.2 + + @computed_field + def tax_due(self) -> float: + return self.total * 0.2 + + @property + def net_total(self) -> float: + return self.total diff --git a/graph/test/python/cases/37-model-signals/src/models.py b/graph/test/python/cases/37-model-signals/src/models.py new file mode 100644 index 00000000..be81f1eb --- /dev/null +++ b/graph/test/python/cases/37-model-signals/src/models.py @@ -0,0 +1,33 @@ +"""Model signals the ORM sends from inside save() and delete() on the client's behalf. + +The client never writes a `send` for these: `Order.objects.create(...)` and +`order.save()` fire post_save with sender=Order. A receiver is scoped by `sender=`. +""" +from django.db import models +from django.db.models.signals import post_save, pre_delete +from django.dispatch import receiver + + +class Order(models.Model): + name = models.CharField(max_length=40) + + +class Invoice(models.Model): + total = models.IntegerField(default=0) + + +def audit_order(sender, instance, created, **kwargs): + return instance + + +post_save.connect(audit_order, sender=Order) + + +@receiver(pre_delete, sender=Order) +def before_order_delete(sender, instance, **kwargs): + return instance + + +@receiver(post_save, sender=Invoice) +def audit_invoice(sender, instance, **kwargs): + return instance diff --git a/graph/test/python/cases/37-model-signals/src/views.py b/graph/test/python/cases/37-model-signals/src/views.py new file mode 100644 index 00000000..64de26fc --- /dev/null +++ b/graph/test/python/cases/37-model-signals/src/views.py @@ -0,0 +1,40 @@ +"""The code that saves and deletes models, and never names a signal.""" +from models import Invoice, Order + + +def create_order(name): + return Order.objects.create(name=name) + + +def rename_order(order: Order, name): + order.name = name + order.save() + + +def drop_order(order: Order): + order.delete() + + +def touch_invoice(invoice: Invoice): + invoice.save() + + +# ── NOT a model signal, and each would be if one condition were dropped ───── +def save_draft(draft): + """The receiver's type is unknown: no model, no sender to match.""" + draft.save() + + +def drop_invoice(invoice: Invoice): + """Invoice has a post_save receiver and no delete receiver.""" + invoice.delete() + + +class Cache: + def save(self): + return None + + +def save_cache(cache: Cache): + """A `save` on a class no receiver names as its sender.""" + cache.save() diff --git a/graph/test/python/cases/38-app-wiring/src/main.py b/graph/test/python/cases/38-app-wiring/src/main.py new file mode 100644 index 00000000..826bb114 --- /dev/null +++ b/graph/test/python/cases/38-app-wiring/src/main.py @@ -0,0 +1,123 @@ +"""Callables an ASGI application registers and the framework calls, with no call site. + +Lifecycle: a lifespan context manager, a middleware class, a middleware function, an +exception handler, a startup handler. Routes: api_route, add_api_route, and a routes +list holding an endpoint class and a websocket function. +""" +from contextlib import asynccontextmanager + +from fastapi import APIRouter, FastAPI +from starlette.applications import Starlette +from starlette.endpoints import HTTPEndpoint +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.routing import Route, WebSocketRoute + + +@asynccontextmanager +async def lifespan(app): + yield + + +app = FastAPI(lifespan=lifespan) + + +class Timing(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + return await call_next(request) + + def label(self): + """Not called by the framework: stays an ordinary method.""" + return "timing" + + +app.add_middleware(Timing) + + +@app.middleware("http") +async def add_header(request, call_next): + return await call_next(request) + + +@app.exception_handler(ValueError) +async def on_value_error(request, exc): + return None + + +@app.on_event("startup") +async def boot(): + return None + + +router = APIRouter() + + +@router.api_route("/bulk", methods=["GET", "POST"]) +async def bulk(): + return [] + + +async def cancel(key: int): + return key + + +router.add_api_route("/orders/{key}/cancel", cancel, methods=["POST"]) + + +class Widgets(HTTPEndpoint): + async def get(self, request): + return None + + async def post(self, request): + return None + + def render(self): + """Not named for a verb: not an entry point.""" + return None + + +async def feed(websocket): + return None + + +async def health(request): + return None + + +routes = [Route("/widgets", Widgets), WebSocketRoute("/feed", endpoint=feed)] +side = Starlette(routes=[Route("/health", health)]) + + +# ── NOT registrations, and each would be if one condition were dropped ────── +class Registry: + def add_route(self, name, fn): + return fn + + def exception_handler(self, fn): + return fn + + +registry = Registry() + + +def plugin_hook(): + return None + + +registry.add_route("pkg.hooks.plugin", plugin_hook) + + +@registry.exception_handler +def bare_handler(exc): + """The decorator names nothing it handles.""" + return exc + + +def configure(lifespan_seconds=0): + return lifespan_seconds + + +def tick(): + return 0 + + +configure(lifespan_seconds=tick) diff --git a/graph/test/python/cases/38-app-wiring/src/shop/__init__.py b/graph/test/python/cases/38-app-wiring/src/shop/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/38-app-wiring/src/shop/cli.py b/graph/test/python/cases/38-app-wiring/src/shop/cli.py new file mode 100644 index 00000000..faef5b20 --- /dev/null +++ b/graph/test/python/cases/38-app-wiring/src/shop/cli.py @@ -0,0 +1,6 @@ +"""A class called Command outside a commands package: not a management command.""" + + +class Command: + def handle(self, *args, **options): + return 0 diff --git a/graph/test/python/cases/38-app-wiring/src/shop/jobs.py b/graph/test/python/cases/38-app-wiring/src/shop/jobs.py new file mode 100644 index 00000000..3594e455 --- /dev/null +++ b/graph/test/python/cases/38-app-wiring/src/shop/jobs.py @@ -0,0 +1,10 @@ +from django.core.management import call_command + + +def nightly(): + call_command("close_orders") + + +def unknown(): + """No command module of this name: nothing to reach.""" + call_command("reopen_orders") diff --git a/graph/test/python/cases/38-app-wiring/src/shop/management/__init__.py b/graph/test/python/cases/38-app-wiring/src/shop/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/38-app-wiring/src/shop/management/commands/__init__.py b/graph/test/python/cases/38-app-wiring/src/shop/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/python/cases/38-app-wiring/src/shop/management/commands/close_orders.py b/graph/test/python/cases/38-app-wiring/src/shop/management/commands/close_orders.py new file mode 100644 index 00000000..a67bee09 --- /dev/null +++ b/graph/test/python/cases/38-app-wiring/src/shop/management/commands/close_orders.py @@ -0,0 +1,13 @@ +"""A management command: the module's file name is the command's name.""" +from django.core.management.base import BaseCommand + +from shop.orders import close_all + + +class Command(BaseCommand): + def handle(self, *args, **options): + return close_all() + + def summary(self): + """Not what the command runner calls.""" + return "close" diff --git a/graph/test/python/cases/38-app-wiring/src/shop/orders.py b/graph/test/python/cases/38-app-wiring/src/shop/orders.py new file mode 100644 index 00000000..25db1397 --- /dev/null +++ b/graph/test/python/cases/38-app-wiring/src/shop/orders.py @@ -0,0 +1,2 @@ +def close_all(): + return 0 diff --git a/graph/test/python/cases/39-model-construction-hooks/src/models.py b/graph/test/python/cases/39-model-construction-hooks/src/models.py new file mode 100644 index 00000000..ae14c75c --- /dev/null +++ b/graph/test/python/cases/39-model-construction-hooks/src/models.py @@ -0,0 +1,62 @@ +"""What building a validated model runs: its validators, its post-init hook and its +default factories. None of them has a call site in the client. +""" +from pydantic import (BaseModel, Field, field_serializer, field_validator, + model_serializer, root_validator, validator) + + +def new_id() -> str: + return "w-1" + + +def new_tags(): + return [] + + +class Widget(BaseModel): + id: str = Field(default_factory=new_id) + size: int + + @field_validator("size") + @classmethod + def check_size(cls, v): + return max(v, 0) + + @validator("id") + def check_id(cls, v): + return v.strip() + + @root_validator + def check_all(cls, values): + return values + + @field_serializer("size") + def dump_size(self, v): + """Runs on dump, not on construction: an entry point, not reached by building.""" + return str(v) + + @model_serializer + def dump(self): + return {} + + def model_post_init(self, ctx): + self.size = self.size or 1 + + +class Gizmo(Widget): + tags: list = Field(default_factory=new_tags) + + +class Plain: + """Not a model: a method of this name is an ordinary method.""" + + def model_post_init(self, ctx): + return ctx + + +class Gadget: + def __init__(self, size): + self.size = self.check_size(size) + + def check_size(self, v): + return max(v, 0) diff --git a/graph/test/python/cases/39-model-construction-hooks/src/service.py b/graph/test/python/cases/39-model-construction-hooks/src/service.py new file mode 100644 index 00000000..e726643e --- /dev/null +++ b/graph/test/python/cases/39-model-construction-hooks/src/service.py @@ -0,0 +1,21 @@ +from models import Gadget, Gizmo, Plain, Widget + + +def make_widget(data: dict): + return Widget(**data) + + +def load_widget(data: dict): + return Widget.model_validate(data) + + +def make_gizmo(): + return Gizmo(size=1) + + +def make_gadget(size: int): + return Gadget(size) + + +def make_plain(): + return Plain() diff --git a/graph/test/python/cases/40-empty-route-path/src/main.py b/graph/test/python/cases/40-empty-route-path/src/main.py new file mode 100644 index 00000000..d47099ca --- /dev/null +++ b/graph/test/python/cases/40-empty-route-path/src/main.py @@ -0,0 +1,94 @@ +"""A route registered with an EMPTY path, and the decorators that look like it. + +A router or blueprint that carries a prefix serves the prefix itself with an empty +path: `APIRouter(prefix="/orders")` plus `@router.get("")` is `GET /orders`, and a +Flask blueprint with `url_prefix` does the same with `@bp.route("")`. The framework +invokes the handler on a request exactly as it does for `@router.get("/{key}")`. + +The negative half: an empty string is accepted only as the FIRST POSITIONAL argument +of a catalogued . decorator. Everything below the second rule would +become an entry point if one of those conditions were dropped. +""" +from unittest import mock + + +class _Router: + """Stands in for a FastAPI router / Flask blueprint; no framework is staged.""" + def route(self, path, *a, **kw): return lambda f: f + def get(self, path, *a, **kw): return lambda f: f + def post(self, path, *a, **kw): return lambda f: f + def put(self, path, *a, **kw): return lambda f: f + def delete(self, path, *a, **kw): return lambda f: f + def patch(self, path, *a, **kw): return lambda f: f + def memoize(self, key): return lambda f: f + + +router = _Router() +bp = _Router() +cache = _Router() + + +def get(path): + return lambda f: f + + +# ── the real thing: the collection endpoints of a prefixed router ──────────── +@router.get("") +def list_orders(): + return [] + + +@router.post("") +def create_order(body): + return body + + +@router.patch('') +def patch_orders(body): + return body + + +@bp.route("") +def list_widgets(): + return "[]" + + +# ── the sibling that already worked, for comparison ───────────────────────── +@router.get("/{key}") +def get_order(key): + return key + + +# ── NOT routes ────────────────────────────────────────────────────────────── +@mock.patch("main.helper") +def patched_test(_m): + """. holds; the argument is a dotted target, not a path.""" + return None + + +@cache.memoize("") +def memoized(): + """Empty first argument, but `memoize` is not an HTTP verb.""" + return None + + +@get("") +def bare_verb(): + """Empty path on a bare `@get` with no receiver: a local helper, not a route.""" + return None + + +@router.delete("orders", "") +def empty_second_argument(): + """The empty string is not the path argument; neither argument starts with "/".""" + return None + + +@router.put(path="") +def keyword_path(): + """Keyword path: excluded for "" exactly as a keyword "/x" is excluded today.""" + return None + + +def helper(): + return "helper" diff --git a/graph/test/python/cases/41-di-marker-forms/src/deps.py b/graph/test/python/cases/41-di-marker-forms/src/deps.py new file mode 100644 index 00000000..05cfefe4 --- /dev/null +++ b/graph/test/python/cases/41-di-marker-forms/src/deps.py @@ -0,0 +1,68 @@ +"""Providers, the classes a marker can name, and module-level Annotated aliases. + +An alias is a module-level VALUE, and the handler that uses it usually imports it: +`CartDep = Annotated[dict, Depends(load_cart)]` here, `cart: CartDep` in handlers.py. +""" +from typing import Annotated + +from fastapi import Depends + + +def load_user(): + return {} + + +def load_owner(): + return {} + + +def load_cart(): + return {} + + +def load_shape_size(): + return 1 + + +def check_region(): + return None + + +def check_quota(): + return None + + +def check_app(): + return None + + +class WidgetFilter: + def __init__(self, q: str = ""): + self.q = q + + +class ColorFilter: + def __init__(self, color: str = ""): + self.color = color + + +class ShapeFilter: + def __init__(self, size: int = Depends(load_shape_size)): + self.size = size + + +class QuotaCheck: + def __call__(self, n: int = 0): + return n < 5 + + +class RateCheck: + """A second __call__ nothing hands to a marker: it must stay unreached.""" + + def __call__(self, n: int = 0): + return n < 9 + + +quota = QuotaCheck() +CartDep = Annotated[dict, Depends(load_cart)] +ShapeDep = Annotated[ShapeFilter, Depends()] diff --git a/graph/test/python/cases/41-di-marker-forms/src/handlers.py b/graph/test/python/cases/41-di-marker-forms/src/handlers.py new file mode 100644 index 00000000..6b756d81 --- /dev/null +++ b/graph/test/python/cases/41-di-marker-forms/src/handlers.py @@ -0,0 +1,69 @@ +"""Every place FastAPI reads a Depends marker, and every kind of provider it takes. + +Each handler below is guarded by exactly the providers its comment names. The +controls at the end are each one condition away from an injection. +""" +from typing import Annotated + +from fastapi import APIRouter, Depends, FastAPI + +from deps import (CartDep, ColorFilter, ShapeDep, WidgetFilter, check_app, + check_quota, check_region, load_owner, load_user, quota) + +app = FastAPI(dependencies=[Depends(check_app)]) +router = APIRouter(dependencies=[Depends(check_region)]) +plain_router = APIRouter() + + +@router.get("/a") +def by_default(user: dict = Depends(load_user)): + """A parameter's default: the form that already worked. Plus check_region.""" + return user + + +@router.get("/b") +def by_annotated(owner: Annotated[dict, Depends(load_owner)]): + """Annotated metadata, written in place. Plus check_region.""" + return owner + + +@router.get("/c") +def by_alias(cart: CartDep): + """An Annotated alias imported from another module. Plus check_region.""" + return cart + + +@router.get("/d", dependencies=[Depends(check_quota)]) +def by_decorator(): + """The route's own dependency list, and the router's. check_quota, check_region.""" + return [] + + +@plain_router.get("/widgets") +def list_widgets(f: WidgetFilter = Depends(WidgetFilter), c: ColorFilter = Depends(), + ok: bool = Depends(quota), s: ShapeDep = None): + """A class, the class shortcut, an instance, and the shortcut through an alias. + + WidgetFilter.__init__, ColorFilter.__init__, QuotaCheck.__call__ and + ShapeFilter.__init__; NOT RateCheck.__call__, and NOT check_region: this router + has no dependency list. + """ + return f, c, ok, s + + +@plain_router.get("/kw") +def by_keyword(owner: dict = Depends(dependency=load_owner)): + """The provider passed by keyword.""" + return owner + + +# ── NOT injection, and each would be if one condition were dropped ────────── +@plain_router.get("/meta") +def plain_metadata(n: Annotated[int, "a note"], m: Annotated[dict, load_user]): + """Annotated metadata with no marker in it: a string, and a bare def.""" + return n, m + + +def bare_untyped(x=Depends()): + """The class shortcut with no annotation names no class.""" + return x diff --git a/graph/test/python/cases/41-di-marker-forms/src/other.py b/graph/test/python/cases/41-di-marker-forms/src/other.py new file mode 100644 index 00000000..b5da2ab0 --- /dev/null +++ b/graph/test/python/cases/41-di-marker-forms/src/other.py @@ -0,0 +1,15 @@ +"""An alias with the SAME NAME as one in deps.py, marking a different provider. + +handlers.py imports `CartDep` from deps, so check_other must stay unreached: the +alias is followed through the import, never matched by its name. +""" +from typing import Annotated + +from fastapi import Depends + + +def check_other(): + return None + + +CartDep = Annotated[dict, Depends(check_other)] diff --git a/graph/test/python/cases/42-signal-forms/src/handlers.py b/graph/test/python/cases/42-signal-forms/src/handlers.py new file mode 100644 index 00000000..76b0f650 --- /dev/null +++ b/graph/test/python/cases/42-signal-forms/src/handlers.py @@ -0,0 +1,45 @@ +"""Every documented way to attach a receiver to a signal declared in another module.""" +from django.dispatch import receiver + +import signals +from signals import order_paid, order_placed, order_shipped + + +@receiver(order_placed) +def on_placed(sender, **kw): + return sender + + +@receiver([order_placed, order_shipped]) +def audit(sender, **kw): + """One receiver, a LIST of signals: it runs for each of them.""" + return sender + + +@receiver(order_paid) +def on_paid(sender, **kw): + return sender + + +@receiver(signals.order_shipped) +def on_shipped_attr(sender, **kw): + """The signal named through its module.""" + return sender + + +def on_closed(sender, **kw): + return sender + + +signals.order_closed.connect(on_closed) + + +# ── NOT receivers, and each would be if one condition were dropped ────────── +def remember(items): + return lambda f: f + + +@remember([order_placed, order_shipped]) +def not_a_receiver(sender, **kw): + """A list of signals handed to a decorator that is not a receiver decorator.""" + return sender diff --git a/graph/test/python/cases/42-signal-forms/src/orders.py b/graph/test/python/cases/42-signal-forms/src/orders.py new file mode 100644 index 00000000..0c8fcf35 --- /dev/null +++ b/graph/test/python/cases/42-signal-forms/src/orders.py @@ -0,0 +1,34 @@ +"""The publishers, in a third module: a name, a module attribute, and the async form.""" +import signals +from signals import order_paid, order_placed, order_voided + + +def place(order): + order_placed.send(sender=order) + + +def ship(order): + signals.order_shipped.send(sender=order) + + +async def pay(order): + await order_paid.asend(sender=order) + + +def close(order): + signals.order_closed.send_robust(sender=order) + + +def void(order): + """A send nothing is attached to: unjoined, as is the Outbox control below.""" + order_voided.send(sender=order) + + +class Outbox: + async def asend(self, sender, **kw): + return sender + + +async def not_a_signal(outbox: Outbox, order): + """An ordinary `asend`: the receiver is not a signal, so no edge (it is listed unjoined).""" + await outbox.asend(sender=order) diff --git a/graph/test/python/cases/42-signal-forms/src/signals.py b/graph/test/python/cases/42-signal-forms/src/signals.py new file mode 100644 index 00000000..0f5db17e --- /dev/null +++ b/graph/test/python/cases/42-signal-forms/src/signals.py @@ -0,0 +1,25 @@ +"""Signals declared in their own module, which every publisher and receiver imports. + +The stand-in class keeps the case self-contained: nothing from the framework is staged. +""" + + +class Signal: + def send(self, sender, **kw): + return sender + + def send_robust(self, sender, **kw): + return sender + + async def asend(self, sender, **kw): + return sender + + def connect(self, fn, sender=None, **kw): + return fn + + +order_placed = Signal() +order_shipped = Signal() +order_paid = Signal() +order_closed = Signal() +order_voided = Signal() 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/08-callables.edges b/graph/test/python/expected/08-callables.edges index a89f21f4..5b0c619d 100644 --- a/graph/test/python/expected/08-callables.edges +++ b/graph/test/python/expected/08-callables.edges @@ -1,7 +1,11 @@ +ambiguous_unknown SELF_CALL main.Holder.use_partial -> - +boundary_lib METHOD_CALL main.Holder.__init__ -> external:functools.partial boundary_lib METHOD_CALL main.main -> external:functools.partial boundary_lib SIMPLE_CALL main.main -> builtin:print known_edge CHAINED_CALL main.main -> main.Holder.use +known_edge CHAINED_CALL main.main -> main.Holder.use_partial known_edge SELF_CALL main.Holder.use -> main.add +known_edge SELF_CALL main.Holder.use_partial -> main.add known_edge SIMPLE_CALL main. -> main.main known_edge SIMPLE_CALL main.main -> main.Holder.__init__ known_edge SIMPLE_CALL main.main -> main.Multiplier.__call__ diff --git a/graph/test/python/expected/08-callables.tiers b/graph/test/python/expected/08-callables.tiers index 8c81e90f..9d83aad2 100644 --- a/graph/test/python/expected/08-callables.tiers +++ b/graph/test/python/expected/08-callables.tiers @@ -1,26 +1,28 @@ -distinct call sites emitted: 13 +distinct call sites emitted: 20 --- by tier: edge ROWS, and the distinct SITES they cover --- - 5 rows 5 sites boundary_lib - 8 rows 8 sites known_edge + 1 rows 1 sites ambiguous_unknown + 8 rows 8 sites boundary_lib + 11 rows 11 sites known_edge --- edge rows by call kind --- - 1 CHAINED_CALL - 1 METHOD_CALL - 1 SELF_CALL - 9 SIMPLE_CALL + 2 CHAINED_CALL + 3 METHOD_CALL + 3 SELF_CALL + 11 SIMPLE_CALL 1 SUBSCRIPT_CALL --- unresolved reasons --- - (none — every site resolved) + 1 no_rule --- the engine's own conservation ledger --- - 13 _total_sites - 5 boundary_lib - 8 known_edge + 20 _total_sites + 1 ambiguous_unknown + 8 boundary_lib + 11 known_edge --- reconciling rows against the conserved site count --- - edge rows 13 + edge rows 20 minus extra rows from multi-target sites 0 - = tier/site pairs 13 - engine's conserved site total 13 + = tier/site pairs 20 + engine's conserved site total 20 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.framework b/graph/test/python/expected/21-url-and-signal-dispatch.framework index 24cf8eba..620020a4 100644 --- a/graph/test/python/expected/21-url-and-signal-dispatch.framework +++ b/graph/test/python/expected/21-url-and-signal-dispatch.framework @@ -7,8 +7,8 @@ url_dispatch registered main. (main.py) -> main.legacy (main.py) [route_table] url_dispatch registered urls. (urls.py) -> views.profile (views.py) [/profile//] url_dispatch registered urls. (urls.py) -> views.settings_page (views.py) [/settings/] -── framework_unjoined (2) ── - 2 signal_dispatch no_receiver +── framework_unjoined (1) ── + 1 signal_dispatch no_receiver ── remote_edge (0) ── ── remote_unserved (0) ── ── remote_unsent (0) ── 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/29-framework-edge-consumers.framework b/graph/test/python/expected/29-framework-edge-consumers.framework index 5c99eef8..6d03b6c1 100644 --- a/graph/test/python/expected/29-framework-edge-consumers.framework +++ b/graph/test/python/expected/29-framework-edge-consumers.framework @@ -2,8 +2,7 @@ signal_dispatch registered app.models.Order.place (app/models.py) -> app.handlers.on_placed (app/handlers.py) [signal] task_dispatch registered app.producer.enqueue (app/producer.py) -> app.tasks.send_report (app/tasks.py) [delay] task_dispatch registered app.producer.reschedule (app/producer.py) -> app.tasks.retry_report (app/tasks.py) [apply_async] -── framework_unjoined (3) ── - 1 signal_dispatch no_receiver +── framework_unjoined (2) ── 2 task_dispatch delay ── remote_edge (0) ── ── remote_unserved (0) ── 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/35-class-attribute-instances.edges b/graph/test/python/expected/35-class-attribute-instances.edges new file mode 100644 index 00000000..e5453a7a --- /dev/null +++ b/graph/test/python/expected/35-class-attribute-instances.edges @@ -0,0 +1,10 @@ +ambiguous_unknown METHOD_CALL shop.models.audit_via_class -> - +ambiguous_unknown METHOD_CALL shop.models.kind_via_class -> - +boundary_lib SIMPLE_CALL shop.models.Order. -> builtin:object.__init__ +boundary_lib SIMPLE_CALL shop.models.Order.__init__ -> builtin:object.__init__ +known_edge METHOD_CALL main.run -> shop.models.OrderManager.open_orders +known_edge METHOD_CALL shop.models.list_via_class -> shop.models.OrderManager.open_orders +known_edge METHOD_CALL shop.models.list_via_instance -> shop.models.OrderManager.open_orders +known_edge METHOD_CALL shop.models.list_via_subclass -> shop.models.OrderManager.open_orders +known_edge SIMPLE_CALL main.run -> shop.models.list_via_class +known_edge SIMPLE_CALL shop.models.list_via_instance -> shop.models.Order.__init__ diff --git a/graph/test/python/expected/35-class-attribute-instances.entries b/graph/test/python/expected/35-class-attribute-instances.entries new file mode 100644 index 00000000..276e39fa --- /dev/null +++ b/graph/test/python/expected/35-class-attribute-instances.entries @@ -0,0 +1 @@ +── entry_point (0) ── diff --git a/graph/test/python/expected/35-class-attribute-instances.framework b/graph/test/python/expected/35-class-attribute-instances.framework new file mode 100644 index 00000000..b9cbca06 --- /dev/null +++ b/graph/test/python/expected/35-class-attribute-instances.framework @@ -0,0 +1,5 @@ +── framework_edge (0) ── +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/35-class-attribute-instances.tiers b/graph/test/python/expected/35-class-attribute-instances.tiers new file mode 100644 index 00000000..18e130a8 --- /dev/null +++ b/graph/test/python/expected/35-class-attribute-instances.tiers @@ -0,0 +1,25 @@ +distinct call sites emitted: 10 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 2 rows 2 sites ambiguous_unknown + 2 rows 2 sites boundary_lib + 6 rows 6 sites known_edge + +--- edge rows by call kind --- + 6 METHOD_CALL + 4 SIMPLE_CALL + +--- unresolved reasons --- + 2 no_rule + +--- the engine's own conservation ledger --- + 10 _total_sites + 2 ambiguous_unknown + 2 boundary_lib + 6 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 10 + minus extra rows from multi-target sites 0 + = tier/site pairs 10 + engine's conserved site total 10 diff --git a/graph/test/python/expected/36-call-through-model-decorators.edges b/graph/test/python/expected/36-call-through-model-decorators.edges new file mode 100644 index 00000000..266e2812 --- /dev/null +++ b/graph/test/python/expected/36-call-through-model-decorators.edges @@ -0,0 +1,17 @@ +ambiguous_unknown DECORATOR_APPLICATION app.orders. -> - +ambiguous_unknown DECORATOR_BARE app.orders. -> - +ambiguous_unknown DECORATOR_BARE app.orders.Order. -> - +boundary_lib DECORATOR_BARE app.orders.Order. -> builtin:property +boundary_lib DECORATOR_CALL app.orders. -> external:pydantic.validate_call +boundary_lib METHOD_CALL app.checkout.show -> builtin:float.hex +boundary_lib SIMPLE_CALL app.orders.apply_discount -> builtin:round +boundary_lib SIMPLE_CALL app.orders.apply_tax -> builtin:round +known_edge DECORATOR_BARE app.orders. -> app.orders.audit +known_edge PROPERTY_READ app.checkout.show -> app.orders.Order.grand_total +known_edge PROPERTY_READ app.checkout.show -> app.orders.Order.net_total +known_edge PROPERTY_READ app.checkout.show -> app.orders.Order.tax_due +known_edge SIMPLE_CALL app.checkout.checkout -> app.orders.apply_discount +known_edge SIMPLE_CALL app.checkout.checkout_coupon -> app.orders.apply_coupon +known_edge SIMPLE_CALL app.checkout.checkout_replaced -> app.orders.audit..wrapper +known_edge SIMPLE_CALL app.checkout.checkout_taxed -> app.orders.apply_tax +known_edge SIMPLE_CALL app.orders.audit..wrapper -> app.orders.replaced diff --git a/graph/test/python/expected/36-call-through-model-decorators.entries b/graph/test/python/expected/36-call-through-model-decorators.entries new file mode 100644 index 00000000..276e39fa --- /dev/null +++ b/graph/test/python/expected/36-call-through-model-decorators.entries @@ -0,0 +1 @@ +── entry_point (0) ── diff --git a/graph/test/python/expected/36-call-through-model-decorators.framework b/graph/test/python/expected/36-call-through-model-decorators.framework new file mode 100644 index 00000000..b9cbca06 --- /dev/null +++ b/graph/test/python/expected/36-call-through-model-decorators.framework @@ -0,0 +1,5 @@ +── framework_edge (0) ── +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/36-call-through-model-decorators.tiers b/graph/test/python/expected/36-call-through-model-decorators.tiers new file mode 100644 index 00000000..2e14b72c --- /dev/null +++ b/graph/test/python/expected/36-call-through-model-decorators.tiers @@ -0,0 +1,31 @@ +distinct call sites emitted: 19 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 4 rows 4 sites ambiguous_unknown + 6 rows 6 sites boundary_lib + 9 rows 9 sites known_edge + +--- edge rows by call kind --- + 1 DECORATOR_APPLICATION + 6 DECORATOR_BARE + 1 DECORATOR_CALL + 1 METHOD_CALL + 3 PROPERTY_READ + 7 SIMPLE_CALL + +--- unresolved reasons --- + 1 decorator_factory_result_untyped + 3 unmodelled_decorator + +--- the engine's own conservation ledger --- + 16 _total_sites + 4 ambiguous_unknown + 6 boundary_lib + 6 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 19 + minus extra rows from multi-target sites 0 + = tier/site pairs 19 + engine's conserved site total 16 + of which 3 are PROPERTY_READ, an edge with no call site (README decision 5) diff --git a/graph/test/python/expected/37-model-signals.edges b/graph/test/python/expected/37-model-signals.edges new file mode 100644 index 00000000..67531829 --- /dev/null +++ b/graph/test/python/expected/37-model-signals.edges @@ -0,0 +1,12 @@ +ambiguous_unknown DECORATOR_APPLICATION models. -> - +ambiguous_unknown METHOD_CALL views.create_order -> - +ambiguous_unknown METHOD_CALL views.drop_invoice -> - +ambiguous_unknown METHOD_CALL views.drop_order -> - +ambiguous_unknown METHOD_CALL views.rename_order -> - +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.entries b/graph/test/python/expected/37-model-signals.entries new file mode 100644 index 00000000..1ba1a65f --- /dev/null +++ b/graph/test/python/expected/37-model-signals.entries @@ -0,0 +1,4 @@ +── entry_point (3) ── + signal_receiver models.audit_invoice models.py:32 + signal_receiver models.audit_order models.py:19 + signal_receiver models.before_order_delete models.py:27 diff --git a/graph/test/python/expected/37-model-signals.framework b/graph/test/python/expected/37-model-signals.framework new file mode 100644 index 00000000..93506d14 --- /dev/null +++ b/graph/test/python/expected/37-model-signals.framework @@ -0,0 +1,9 @@ +── framework_edge (4) ── + signal_dispatch registered views.create_order (views.py) -> models.audit_order (models.py) [post_save] + signal_dispatch registered views.drop_order (views.py) -> models.before_order_delete (models.py) [pre_delete] + signal_dispatch registered views.rename_order (views.py) -> models.audit_order (models.py) [post_save] + signal_dispatch registered views.touch_invoice (views.py) -> models.audit_invoice (models.py) [post_save] +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/37-model-signals.tiers b/graph/test/python/expected/37-model-signals.tiers new file mode 100644 index 00000000..e87fd3fa --- /dev/null +++ b/graph/test/python/expected/37-model-signals.tiers @@ -0,0 +1,29 @@ +distinct call sites emitted: 14 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 8 rows 8 sites ambiguous_unknown + 5 rows 5 sites boundary_lib + 1 rows 1 sites known_edge + +--- edge rows by call kind --- + 2 DECORATOR_APPLICATION + 2 DECORATOR_CALL + 10 METHOD_CALL + +--- unresolved reasons --- + 2 decorator_factory_result_untyped + 4 escape_hatch + 1 untyped_receiver:class_attribute_absent + 1 untyped_receiver:parameter + +--- the engine's own conservation ledger --- + 14 _total_sites + 8 ambiguous_unknown + 5 boundary_lib + 1 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 14 + minus extra rows from multi-target sites 0 + = tier/site pairs 14 + engine's conserved site total 14 diff --git a/graph/test/python/expected/38-app-wiring.edges b/graph/test/python/expected/38-app-wiring.edges new file mode 100644 index 00000000..39d93d28 --- /dev/null +++ b/graph/test/python/expected/38-app-wiring.edges @@ -0,0 +1,22 @@ +ambiguous_unknown DECORATOR_APPLICATION main. -> - +ambiguous_unknown DECORATOR_ATTRIBUTE main. -> - +ambiguous_unknown DECORATOR_BARE main. -> - +ambiguous_unknown SIMPLE_CALL main.Timing.dispatch -> - +ambiguous_unknown SIMPLE_CALL main.add_header -> - +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 +boundary_lib DECORATOR_CALL main. -> external:FastAPI.on_event +boundary_lib METHOD_CALL main. -> external:APIRouter.add_api_route +boundary_lib METHOD_CALL main. -> external:FastAPI.add_middleware +boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ +boundary_lib SIMPLE_CALL main. -> external:APIRouter +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.entries b/graph/test/python/expected/38-app-wiring.entries new file mode 100644 index 00000000..566d43cc --- /dev/null +++ b/graph/test/python/expected/38-app-wiring.entries @@ -0,0 +1,13 @@ +── entry_point (12) ── + cli shop.management.commands.close_orders.Command.handle shop/management/commands/close_orders.py:8 + http main.bulk main.py:55 + http main.cancel main.py:59 + lifecycle main.Timing.dispatch main.py:25 + lifecycle main.add_header main.py:37 + lifecycle main.boot main.py:47 + lifecycle main.lifespan main.py:17 + lifecycle main.on_value_error main.py:42 + url main.Widgets.get main.py:67 + url main.Widgets.post main.py:70 + url main.feed main.py:78 + url main.health main.py:82 diff --git a/graph/test/python/expected/38-app-wiring.framework b/graph/test/python/expected/38-app-wiring.framework new file mode 100644 index 00000000..8c42c13f --- /dev/null +++ b/graph/test/python/expected/38-app-wiring.framework @@ -0,0 +1,10 @@ +── framework_edge (5) ── + command_dispatch registered shop.jobs.nightly (shop/jobs.py) -> shop.management.commands.close_orders.Command.handle (shop/management/commands/close_orders.py) [close_orders] + url_dispatch registered main. (main.py) -> main.Widgets.get (main.py) [/widgets] + url_dispatch registered main. (main.py) -> main.Widgets.post (main.py) [/widgets] + url_dispatch registered main. (main.py) -> main.feed (main.py) [/feed] + url_dispatch registered main. (main.py) -> main.health (main.py) [/health] +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/38-app-wiring.tiers b/graph/test/python/expected/38-app-wiring.tiers new file mode 100644 index 00000000..8496a4b9 --- /dev/null +++ b/graph/test/python/expected/38-app-wiring.tiers @@ -0,0 +1,31 @@ +distinct call sites emitted: 26 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 8 rows 8 sites ambiguous_unknown + 15 rows 15 sites boundary_lib + 3 rows 3 sites known_edge + +--- edge rows by call kind --- + 4 DECORATOR_APPLICATION + 1 DECORATOR_ATTRIBUTE + 1 DECORATOR_BARE + 4 DECORATOR_CALL + 3 METHOD_CALL + 13 SIMPLE_CALL + +--- unresolved reasons --- + 2 callee_is_parameter + 4 decorator_factory_result_untyped + 2 unmodelled_decorator + +--- the engine's own conservation ledger --- + 26 _total_sites + 8 ambiguous_unknown + 15 boundary_lib + 3 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 26 + minus extra rows from multi-target sites 0 + = tier/site pairs 26 + engine's conserved site total 26 diff --git a/graph/test/python/expected/39-model-construction-hooks.edges b/graph/test/python/expected/39-model-construction-hooks.edges new file mode 100644 index 00000000..3c8b20b6 --- /dev/null +++ b/graph/test/python/expected/39-model-construction-hooks.edges @@ -0,0 +1,18 @@ +ambiguous_unknown DECORATOR_APPLICATION models.Widget. -> - +ambiguous_unknown DECORATOR_BARE models.Widget. -> - +ambiguous_unknown METHOD_CALL models.Widget.check_id -> - +ambiguous_unknown METHOD_CALL service.load_widget -> - +ambiguous_unknown SIMPLE_CALL service.make_widget -> - +boundary_lib DECORATOR_BARE models.Widget. -> builtin:classmethod +boundary_lib DECORATOR_CALL models.Widget. -> external:field_serializer +boundary_lib DECORATOR_CALL models.Widget. -> external:field_validator +boundary_lib DECORATOR_CALL models.Widget. -> external:validator +boundary_lib SIMPLE_CALL models.Gadget.check_size -> builtin:max +boundary_lib SIMPLE_CALL models.Gizmo. -> external:Field +boundary_lib SIMPLE_CALL models.Widget. -> external:Field +boundary_lib SIMPLE_CALL models.Widget.check_size -> builtin:max +boundary_lib SIMPLE_CALL models.Widget.dump_size -> builtin:str +boundary_lib SIMPLE_CALL service.make_gizmo -> builtin:object.__init__ +boundary_lib SIMPLE_CALL service.make_plain -> builtin:object.__init__ +known_edge SELF_CALL models.Gadget.__init__ -> models.Gadget.check_size +known_edge SIMPLE_CALL service.make_gadget -> models.Gadget.__init__ diff --git a/graph/test/python/expected/39-model-construction-hooks.entries b/graph/test/python/expected/39-model-construction-hooks.entries new file mode 100644 index 00000000..18c0f2d8 --- /dev/null +++ b/graph/test/python/expected/39-model-construction-hooks.entries @@ -0,0 +1,7 @@ +── entry_point (6) ── + orm_hook models.Widget.check_all models.py:30 + orm_hook models.Widget.check_id models.py:26 + orm_hook models.Widget.check_size models.py:22 + orm_hook models.Widget.dump models.py:39 + orm_hook models.Widget.dump_size models.py:34 + orm_hook models.Widget.model_post_init models.py:42 diff --git a/graph/test/python/expected/39-model-construction-hooks.framework b/graph/test/python/expected/39-model-construction-hooks.framework new file mode 100644 index 00000000..394cd852 --- /dev/null +++ b/graph/test/python/expected/39-model-construction-hooks.framework @@ -0,0 +1,21 @@ +── framework_edge (16) ── + model_hook registered service.load_widget (service.py) -> models.Widget.check_all (models.py) [root_validator] + model_hook registered service.load_widget (service.py) -> models.Widget.check_id (models.py) [validator] + model_hook registered service.load_widget (service.py) -> models.Widget.check_size (models.py) [field_validator] + model_hook registered service.load_widget (service.py) -> models.Widget.model_post_init (models.py) [model_post_init] + model_hook registered service.load_widget (service.py) -> models.new_id (models.py) [default_factory] + model_hook registered service.make_gizmo (service.py) -> models.Widget.check_all (models.py) [root_validator] + model_hook registered service.make_gizmo (service.py) -> models.Widget.check_id (models.py) [validator] + model_hook registered service.make_gizmo (service.py) -> models.Widget.check_size (models.py) [field_validator] + model_hook registered service.make_gizmo (service.py) -> models.Widget.model_post_init (models.py) [model_post_init] + model_hook registered service.make_gizmo (service.py) -> models.new_id (models.py) [default_factory] + model_hook registered service.make_gizmo (service.py) -> models.new_tags (models.py) [default_factory] + model_hook registered service.make_widget (service.py) -> models.Widget.check_all (models.py) [root_validator] + model_hook registered service.make_widget (service.py) -> models.Widget.check_id (models.py) [validator] + model_hook registered service.make_widget (service.py) -> models.Widget.check_size (models.py) [field_validator] + model_hook registered service.make_widget (service.py) -> models.Widget.model_post_init (models.py) [model_post_init] + model_hook registered service.make_widget (service.py) -> models.new_id (models.py) [default_factory] +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/39-model-construction-hooks.tiers b/graph/test/python/expected/39-model-construction-hooks.tiers new file mode 100644 index 00000000..a4e4b8af --- /dev/null +++ b/graph/test/python/expected/39-model-construction-hooks.tiers @@ -0,0 +1,33 @@ +distinct call sites emitted: 21 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 8 rows 8 sites ambiguous_unknown + 11 rows 11 sites boundary_lib + 2 rows 2 sites known_edge + +--- edge rows by call kind --- + 3 DECORATOR_APPLICATION + 3 DECORATOR_BARE + 3 DECORATOR_CALL + 2 METHOD_CALL + 1 SELF_CALL + 9 SIMPLE_CALL + +--- unresolved reasons --- + 1 construction_of_dynamically_extended_class + 3 decorator_factory_result_untyped + 1 no_rule + 2 unmodelled_decorator + 1 untyped_receiver:parameter + +--- the engine's own conservation ledger --- + 21 _total_sites + 8 ambiguous_unknown + 11 boundary_lib + 2 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 21 + minus extra rows from multi-target sites 0 + = tier/site pairs 21 + engine's conserved site total 21 diff --git a/graph/test/python/expected/40-empty-route-path.edges b/graph/test/python/expected/40-empty-route-path.edges new file mode 100644 index 00000000..052b5acb --- /dev/null +++ b/graph/test/python/expected/40-empty-route-path.edges @@ -0,0 +1,11 @@ +ambiguous_unknown DECORATOR_APPLICATION main. -> - +boundary_lib DECORATOR_CALL main. -> external:mock.patch +boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ +known_edge DECORATOR_CALL main. -> main._Router.delete +known_edge DECORATOR_CALL main. -> main._Router.get +known_edge DECORATOR_CALL main. -> main._Router.memoize +known_edge DECORATOR_CALL main. -> main._Router.patch +known_edge DECORATOR_CALL main. -> main._Router.post +known_edge DECORATOR_CALL main. -> main._Router.put +known_edge DECORATOR_CALL main. -> main._Router.route +known_edge DECORATOR_CALL main. -> main.get diff --git a/graph/test/python/expected/40-empty-route-path.entries b/graph/test/python/expected/40-empty-route-path.entries new file mode 100644 index 00000000..b0166047 --- /dev/null +++ b/graph/test/python/expected/40-empty-route-path.entries @@ -0,0 +1,6 @@ +── entry_point (5) ── + http main.create_order main.py:42 + http main.get_order main.py:58 + http main.list_orders main.py:37 + http main.list_widgets main.py:52 + http main.patch_orders main.py:47 diff --git a/graph/test/python/expected/40-empty-route-path.framework b/graph/test/python/expected/40-empty-route-path.framework new file mode 100644 index 00000000..b9cbca06 --- /dev/null +++ b/graph/test/python/expected/40-empty-route-path.framework @@ -0,0 +1,5 @@ +── framework_edge (0) ── +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/40-empty-route-path.tiers b/graph/test/python/expected/40-empty-route-path.tiers new file mode 100644 index 00000000..fa837ac6 --- /dev/null +++ b/graph/test/python/expected/40-empty-route-path.tiers @@ -0,0 +1,26 @@ +distinct call sites emitted: 23 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 10 rows 10 sites ambiguous_unknown + 4 rows 4 sites boundary_lib + 9 rows 9 sites known_edge + +--- edge rows by call kind --- + 10 DECORATOR_APPLICATION + 10 DECORATOR_CALL + 3 SIMPLE_CALL + +--- unresolved reasons --- + 10 decorator_factory_result_untyped + +--- the engine's own conservation ledger --- + 23 _total_sites + 10 ambiguous_unknown + 4 boundary_lib + 9 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 23 + minus extra rows from multi-target sites 0 + = tier/site pairs 23 + engine's conserved site total 23 diff --git a/graph/test/python/expected/41-di-marker-forms.edges b/graph/test/python/expected/41-di-marker-forms.edges new file mode 100644 index 00000000..0a449dee --- /dev/null +++ b/graph/test/python/expected/41-di-marker-forms.edges @@ -0,0 +1,10 @@ +ambiguous_unknown DECORATOR_APPLICATION handlers. -> - +boundary_lib DECORATOR_CALL handlers. -> external:APIRouter.get +boundary_lib DECORATOR_CALL handlers. -> external:Depends +boundary_lib SIMPLE_CALL deps. -> builtin:object.__init__ +boundary_lib SIMPLE_CALL deps. -> external:Depends +boundary_lib SIMPLE_CALL deps.ShapeFilter. -> external:Depends +boundary_lib SIMPLE_CALL handlers. -> external:APIRouter +boundary_lib SIMPLE_CALL handlers. -> external:Depends +boundary_lib SIMPLE_CALL handlers. -> external:FastAPI +boundary_lib SIMPLE_CALL other. -> external:Depends diff --git a/graph/test/python/expected/41-di-marker-forms.entries b/graph/test/python/expected/41-di-marker-forms.entries new file mode 100644 index 00000000..de8fa537 --- /dev/null +++ b/graph/test/python/expected/41-di-marker-forms.entries @@ -0,0 +1,19 @@ +── entry_point (18) ── + di_provider deps.ColorFilter.__init__ deps.py:45 + di_provider deps.QuotaCheck.__call__ deps.py:55 + di_provider deps.ShapeFilter.__init__ deps.py:50 + di_provider deps.WidgetFilter.__init__ deps.py:40 + di_provider deps.check_app deps.py:35 + di_provider deps.check_quota deps.py:31 + di_provider deps.check_region deps.py:27 + di_provider deps.load_cart deps.py:19 + di_provider deps.load_owner deps.py:15 + di_provider deps.load_shape_size deps.py:23 + di_provider deps.load_user deps.py:11 + http handlers.by_alias handlers.py:31 + http handlers.by_annotated handlers.py:25 + http handlers.by_decorator handlers.py:37 + http handlers.by_default handlers.py:19 + http handlers.by_keyword handlers.py:55 + http handlers.list_widgets handlers.py:43 + http handlers.plain_metadata handlers.py:62 diff --git a/graph/test/python/expected/41-di-marker-forms.framework b/graph/test/python/expected/41-di-marker-forms.framework new file mode 100644 index 00000000..4c301663 --- /dev/null +++ b/graph/test/python/expected/41-di-marker-forms.framework @@ -0,0 +1,19 @@ +── framework_edge (14) ── + di_provider registered deps.ShapeFilter.__init__ (deps.py) -> deps.load_shape_size (deps.py) [Depends] + di_provider registered handlers.by_alias (handlers.py) -> deps.check_region (deps.py) [Depends] + di_provider registered handlers.by_alias (handlers.py) -> deps.load_cart (deps.py) [Depends] + di_provider registered handlers.by_annotated (handlers.py) -> deps.check_region (deps.py) [Depends] + di_provider registered handlers.by_annotated (handlers.py) -> deps.load_owner (deps.py) [Depends] + di_provider registered handlers.by_decorator (handlers.py) -> deps.check_quota (deps.py) [Depends] + di_provider registered handlers.by_decorator (handlers.py) -> deps.check_region (deps.py) [Depends] + di_provider registered handlers.by_default (handlers.py) -> deps.check_region (deps.py) [Depends] + di_provider registered handlers.by_default (handlers.py) -> deps.load_user (deps.py) [Depends] + di_provider registered handlers.by_keyword (handlers.py) -> deps.load_owner (deps.py) [Depends] + di_provider registered handlers.list_widgets (handlers.py) -> deps.ColorFilter.__init__ (deps.py) [Depends] + di_provider registered handlers.list_widgets (handlers.py) -> deps.QuotaCheck.__call__ (deps.py) [Depends] + di_provider registered handlers.list_widgets (handlers.py) -> deps.ShapeFilter.__init__ (deps.py) [Depends] + di_provider registered handlers.list_widgets (handlers.py) -> deps.WidgetFilter.__init__ (deps.py) [Depends] +── framework_unjoined (0) ── +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/41-di-marker-forms.tiers b/graph/test/python/expected/41-di-marker-forms.tiers new file mode 100644 index 00000000..85ad730a --- /dev/null +++ b/graph/test/python/expected/41-di-marker-forms.tiers @@ -0,0 +1,24 @@ +distinct call sites emitted: 32 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 7 rows 7 sites ambiguous_unknown + 25 rows 25 sites boundary_lib + +--- edge rows by call kind --- + 7 DECORATOR_APPLICATION + 8 DECORATOR_CALL + 17 SIMPLE_CALL + +--- unresolved reasons --- + 7 decorator_factory_result_untyped + +--- the engine's own conservation ledger --- + 32 _total_sites + 7 ambiguous_unknown + 25 boundary_lib + +--- reconciling rows against the conserved site count --- + edge rows 32 + minus extra rows from multi-target sites 0 + = tier/site pairs 32 + engine's conserved site total 32 diff --git a/graph/test/python/expected/42-signal-forms.edges b/graph/test/python/expected/42-signal-forms.edges new file mode 100644 index 00000000..456f0118 --- /dev/null +++ b/graph/test/python/expected/42-signal-forms.edges @@ -0,0 +1,11 @@ +ambiguous_unknown DECORATOR_APPLICATION handlers. -> - +ambiguous_unknown METHOD_CALL handlers. -> - +ambiguous_unknown METHOD_CALL orders.close -> - +ambiguous_unknown METHOD_CALL orders.ship -> - +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.entries b/graph/test/python/expected/42-signal-forms.entries new file mode 100644 index 00000000..34341537 --- /dev/null +++ b/graph/test/python/expected/42-signal-forms.entries @@ -0,0 +1,6 @@ +── entry_point (5) ── + signal_receiver handlers.audit handlers.py:14 + signal_receiver handlers.on_closed handlers.py:30 + signal_receiver handlers.on_paid handlers.py:20 + signal_receiver handlers.on_placed handlers.py:9 + signal_receiver handlers.on_shipped_attr handlers.py:25 diff --git a/graph/test/python/expected/42-signal-forms.framework b/graph/test/python/expected/42-signal-forms.framework new file mode 100644 index 00000000..f7bc2993 --- /dev/null +++ b/graph/test/python/expected/42-signal-forms.framework @@ -0,0 +1,12 @@ +── framework_edge (6) ── + signal_dispatch registered orders.close (orders.py) -> handlers.on_closed (handlers.py) [signal] + signal_dispatch registered orders.pay (orders.py) -> handlers.on_paid (handlers.py) [signal] + signal_dispatch registered orders.place (orders.py) -> handlers.audit (handlers.py) [signal] + signal_dispatch registered orders.place (orders.py) -> handlers.on_placed (handlers.py) [signal] + signal_dispatch registered orders.ship (orders.py) -> handlers.audit (handlers.py) [signal] + signal_dispatch registered orders.ship (orders.py) -> handlers.on_shipped_attr (handlers.py) [signal] +── framework_unjoined (2) ── + 2 signal_dispatch no_receiver +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/42-signal-forms.tiers b/graph/test/python/expected/42-signal-forms.tiers new file mode 100644 index 00000000..e780b18a --- /dev/null +++ b/graph/test/python/expected/42-signal-forms.tiers @@ -0,0 +1,28 @@ +distinct call sites emitted: 22 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 8 rows 8 sites ambiguous_unknown + 9 rows 9 sites boundary_lib + 5 rows 5 sites known_edge + +--- edge rows by call kind --- + 5 DECORATOR_APPLICATION + 5 DECORATOR_CALL + 7 METHOD_CALL + 5 SIMPLE_CALL + +--- unresolved reasons --- + 5 decorator_factory_result_untyped + 3 untyped_receiver:attribute_object_untyped + +--- the engine's own conservation ledger --- + 22 _total_sites + 8 ambiguous_unknown + 9 boundary_lib + 5 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 22 + minus extra rows from multi-target sites 0 + = tier/site pairs 22 + engine's conserved site total 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 a4bbd62c..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 @@ -61,4 +66,106 @@ if run; then bad "a run with no engine package and no souffle succeeded"; else grep -q "npm install" "$W/log" || bad "the no-package error does not point at npm install" fi -if [ "$fail" -eq 0 ]; then echo "engine-package: ok (packaged engine by id, stale package refused, absence explained)"; else echo "engine-package: $fail failure(s)"; exit 1; fi +# 4-7. `--prepare` (what `axiomcode prepare` runs at build time) puts the binary where a run looks for it, so the first +# index reuses it instead of compiling. A stub souffle and c++ stand in for the real ones: c++ "compiles" the fake +# engine above and counts its calls. Control: a background prepare under CI compiles nothing. +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/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" +{ echo '#!/usr/bin/env bash' + echo "echo x >> '$W/cc-calls'" + echo 'while [ $# -gt 0 ]; do case "$1" in -o) o="$2"; shift 2;; *) shift;; esac; done' + echo "cp '$fake' \"\$o\"; chmod +x \"\$o\""; } > "$W/stub/c++" +chmod +x "$W/stub/souffle" "$W/stub/c++" +prep(){ PATH="$W/stub:$SANDBOX_PATH" AXIOM_SOUFFLE_INCLUDE="$W/inc" AXIOM_SOUFFLE_CACHE="$W/cache" bash "$RUN" --language $lang --prepare > "$W/log" 2>&1; } +calls(){ wc -l < "$W/cc-calls" | tr -d ' '; } +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)" +# the run that follows: no souffle, no package, only the prepared binary — and it is used, not recompiled +rm -rf "$W/out" +if run; then grep -q "reusing cached binary" "$W/log" || bad "the run after prepare did not reuse the prepared binary" +else bad "the run after prepare failed:"; tail -8 "$W/log" | sed 's/^/ /'; fi +# control: the build's background prepare is skipped under CI, so nothing is compiled +rm -rf "$W/cache" +out="$(PATH="$W/stub:$SANDBOX_PATH" CI=1 AXIOM_SOUFFLE_CACHE="$W/cache" bash "$ROOT/bin/axiomcode" prepare --language $lang --background 2>&1)" +sleep 1 +case "$out" in *"not prepared"*) ;; *) bad "background prepare under CI did not say it was skipped: $out";; esac +[ "$(calls)" = 1 ] && [ ! -e "$W/cache/souffle-engine-$lang-$id" ] || bad "background prepare under CI compiled anyway" + +if [ "$fail" -eq 0 ]; then echo "engine-package: ok (packaged engine by id, stale package refused, absence explained, prepared binary reused)"; else echo "engine-package: $fail failure(s)"; exit 1; fi diff --git a/graph/test/tools/xml-id-portable-test.sh b/graph/test/tools/xml-id-portable-test.sh new file mode 100755 index 00000000..63479158 --- /dev/null +++ b/graph/test/tools/xml-id-portable-test.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# AN XML ID MUST NOT DEPEND ON WHERE THE TREE IS CHECKED OUT. +# +# Every XML element, attribute and value-reference id hashed the file's ABSOLUTE path +# (an element's also the absolute project path). A value reference's id is what a +# declared unknown names (config_unresolved spel_expression), so the Java golden that +# printed one passed only in the checkout it was blessed in, CI included. +# +# The ids now hash the path relative to the analysis root. Checked here: +# 1. one tree parsed from two directories gives the same element, attribute and +# value-reference ids; +# 2. CONTROL: two byte-identical files at different relative paths in one tree still +# get different ids, so the key did not simply stop naming the file; +# 3. neither run is empty, so (1) cannot pass on two empty sets. +# ───────────────────────────────────────────────────────────────────────────── +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(d="$HERE"; while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="$(dirname "$d")"; done; echo "$d")" +PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}" +[ -f "$PARSER" ] || { echo "xml-id-portable: SKIP (no parser at $PARSER)"; exit 0; } +command -v node >/dev/null 2>&1 || { echo "xml-id-portable: SKIP (no node)"; exit 0; } + +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +mk() { # mk + local p="$1" + mkdir -p "$p/a/src/main/resources" "$p/b/src/main/resources" + printf '4.0.0\n' > "$p/pom.xml" + local x='\n \n \n \n \n\n' + printf "$x" > "$p/a/src/main/resources/beans.xml" + printf "$x" > "$p/b/src/main/resources/beans.xml" +} +mk "$W/one/proj" +mk "$W/two/deeper/checkout/proj" +node "$PARSER" "$W/one/proj" xmlid false "$W/ir1" >"$W/p1.log" 2>&1 +node "$PARSER" "$W/two/deeper/checkout/proj" xmlid false "$W/ir2" >"$W/p2.log" 2>&1 + +fail=0; bad(){ echo " ✗ $*"; fail=$((fail+1)); } +# The row's own id is the last column of each table. +ids() { [ -f "$1" ] && awk -F'\t' 'NR>1 { print $NF }' "$1" | sort; } +# The ids of the rows of one module's file, by the file path column ($2 = a|b). +ids_of() { [ -f "$1" ] && awk -F'\t' -v m="/$2/src/main/resources/beans.xml" 'NR>1 && index($0, m) { print $NF }' "$1" | sort; } + +for t in all-xml-elements all-xml-attributes all-xml-value-references; do + n1=$(ids "$W/ir1/$t.csv" | grep -c . || true); n2=$(ids "$W/ir2/$t.csv" | grep -c . || true) + [ "${n1:-0}" -ge 2 ] || bad "$t: the first run wrote ${n1:-0} rows, expected at least 2" + [ "${n1:-0}" = "${n2:-0}" ] || bad "$t: ${n1:-0} rows from one directory, ${n2:-0} from the other" + if ! diff -q <(ids "$W/ir1/$t.csv") <(ids "$W/ir2/$t.csv") >/dev/null; then + bad "$t: the same tree got different ids in two directories ($(comm -23 <(ids "$W/ir1/$t.csv") <(ids "$W/ir2/$t.csv") | grep -c .) moved)" + fi + a=$(ids_of "$W/ir1/$t.csv" a); b=$(ids_of "$W/ir1/$t.csv" b) + [ -n "$a" ] && [ -n "$b" ] || bad "$t: no rows for one of the two identical files" + [ -z "$(comm -12 <(printf '%s\n' "$a") <(printf '%s\n' "$b") | grep .)" ] \ + || bad "$t: two identical files at different paths share an id" +done +[ "$(awk -F'\t' 'NR>1 && $2=="SPEL_EXPRESSION"' "$W/ir1/all-xml-value-references.csv" 2>/dev/null | grep -c .)" -ge 2 ] \ + || bad "no SPEL_EXPRESSION value reference was extracted, so the id a declared unknown prints was not checked" + +if [ "$fail" -eq 0 ]; then echo "xml-id-portable: ok (same ids from two directories, identical files at two paths still distinct)"; else echo "xml-id-portable: $fail failure(s)"; exit 1; fi diff --git a/graph/test/typescript/cases/03-structural-satisfaction/src/bus.ts b/graph/test/typescript/cases/03-structural-satisfaction/src/bus.ts new file mode 100644 index 00000000..8392d961 --- /dev/null +++ b/graph/test/typescript/cases/03-structural-satisfaction/src/bus.ts @@ -0,0 +1,10 @@ +import type { Pub } from "./pub"; + +// Sees Pub, shares the name `publish`, and still does not satisfy it: its publish needs +// two arguments where Pub's callers pass one. +export class Bus { + publish(name: string, payload: unknown): void {} +} + +export const bus = new Bus(); +export type Seen = Pub; diff --git a/graph/test/typescript/cases/03-structural-satisfaction/src/declared.ts b/graph/test/typescript/cases/03-structural-satisfaction/src/declared.ts new file mode 100644 index 00000000..e8b111a5 --- /dev/null +++ b/graph/test/typescript/cases/03-structural-satisfaction/src/declared.ts @@ -0,0 +1,12 @@ +// Brings Pub in through a package specifier the engine cannot follow to pub.ts (a +// workspace package whose build output is absent), so the import walk never reaches Pub's +// module. The import still names Pub, and that is the evidence: Declared stays a Pub +// candidate. +// @ts-expect-error the package is not installed in this case +import type { Pub } from "@workspace/pub"; + +export class Declared implements Pub { + publish(e: { id: string }): void {} +} + +export const declared = new Declared(); diff --git a/graph/test/typescript/cases/03-structural-satisfaction/src/duck-pub.ts b/graph/test/typescript/cases/03-structural-satisfaction/src/duck-pub.ts new file mode 100644 index 00000000..6af8cd5e --- /dev/null +++ b/graph/test/typescript/cases/03-structural-satisfaction/src/duck-pub.ts @@ -0,0 +1,18 @@ +import { Relay, type Pub } from "./pub"; + +// CONTROL: no `implements`, an extra optional parameter, and passed as a Pub — it stays +// a structural candidate of Pub.publish. +export class Duck { + publish(e: { id: string }, trace?: string): void {} +} + +// Built beside a Pub and the right arity, but its publish is private: not assignable to Pub. +export class Hidden { + private publish(e: { id: string }): void {} +} +export const hidden = new Hidden(); + +export function wire(): void { + const p: Pub = new Duck(); + new Relay(p).run(); +} diff --git a/graph/test/typescript/cases/03-structural-satisfaction/src/pub.ts b/graph/test/typescript/cases/03-structural-satisfaction/src/pub.ts new file mode 100644 index 00000000..cc19ffde --- /dev/null +++ b/graph/test/typescript/cases/03-structural-satisfaction/src/pub.ts @@ -0,0 +1,27 @@ +// A shared method NAME is not conformance. Pub has a declared implementor (Real) and a +// conformer the program builds and passes as a Pub (Duck, in duck-pub.ts). Two more +// classes share the name `publish` and are NOT Pubs: +// * Bus (bus.ts) sees Pub but its publish needs two arguments — not assignable; +// * Stranger (stranger.ts) has the right arity but no module that holds it ever +// imports this one, so no instance of it can reach a Pub-typed slot. +// The control for Stranger is Declared (declared.ts): it never reaches this module either, +// but it imports a Pub from a package the walk cannot follow, so it stays a candidate. + +export interface Pub { + publish(e: { id: string }): void; +} + +export class Relay { + constructor(private readonly p: Pub) {} + run(): void { + this.p.publish({ id: "1" }); + } +} + +export class Real implements Pub { + publish(e: { id: string }): void {} +} + +export function start(): void { + new Relay(new Real()).run(); +} diff --git a/graph/test/typescript/cases/03-structural-satisfaction/src/stranger.ts b/graph/test/typescript/cases/03-structural-satisfaction/src/stranger.ts new file mode 100644 index 00000000..39267f86 --- /dev/null +++ b/graph/test/typescript/cases/03-structural-satisfaction/src/stranger.ts @@ -0,0 +1,7 @@ +// Same shape as Pub, but nothing that holds a Stranger ever imports pub.ts: no Stranger +// can be handed to code typed by Pub. +export class Stranger { + publish(e: { id: string }): void {} +} + +export const stranger = new Stranger(); diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts index 198ebefa..10e32c63 100644 --- a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts @@ -47,3 +47,22 @@ export function each(xs: number[], fn: (n: number) => void): void { export function useEach(xs: number[]): void { each(xs, (n) => record(n + RATE)) } + +// a host registration with no body: the const handed to it holds a library-wrapped function literal, and +// registering the const reaches that literal as registering the literal bare does +import { migrate, plugin, settings } from './plugin' +declare const host: { register(p: unknown): void } + +export function boot(): void { + host.register(plugin) +} + +// CONTROL: the literal handed bare, already reached from the site that hands it +export function bootBare(): void { + host.register(async () => migrate()) +} + +// CONTROL: a const with no function in it registers nothing +export function bootSettings(): void { + host.register(settings) +} diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts new file mode 100644 index 00000000..3f31a567 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts @@ -0,0 +1,12 @@ +// A FUNCTION WRAPPED BY A LIBRARY CALL AND KEPT IN A CONST. `wrap` is a declaration only, so the const holds +// what a library returns; the function literal it was handed is what a registration of the const runs. +declare function wrap(f: F): F + +export function migrate(): void {} + +export const plugin = wrap(async () => { + migrate() +}) + +// CONTROL: a const holding a plain value built by a library call hands over no function +export const settings = wrap(42) diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts new file mode 100644 index 00000000..6ad0b7b8 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts @@ -0,0 +1,25 @@ +// NEAR MISS: a Map or Set stores, finds and drops a function and never calls it, so storing one is not +// handing it over: `set`, `add` and `has` reach nothing. Neither is declared in this case, so each is +// recognised by the reference it was declared with or by the `new` that initialises it. +function onUpdated(): number { + return 1 +} + +const handlers = new Map() +const subs: Set<() => number> = new Set() + +export function store(): void { + handlers.set('a', onUpdated) + subs.add(onUpdated) +} + +export function known(): boolean { + return subs.has(onUpdated) +} + +// CONTROL: a host API with no body that is handed the same function still reaches it. +declare function later(cb: () => number): void + +export function deferred(): void { + later(onUpdated) +} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/factory.ts b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/factory.ts new file mode 100644 index 00000000..0b9ab952 --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/factory.ts @@ -0,0 +1,28 @@ +// ============================================================================ +// CASE 82 — `call` / `apply` / `bind` in a project whose tsconfigs DISAGREE +// ============================================================================ +// Cases 21 and 22 each compile under ONE setting of `strictBindCallApply`. This one +// has two programs: `src/tsconfig.json` (strict) governs this file and `strict-site.ts`, +// and `src/loose/tsconfig.json` (strictBindCallApply: false) governs `loose/`. The +// same call written in each answers with a different declaration: +// +// strict-site.ts -> CallableFunction#bind / #call / #apply +// loose/loose-site.ts -> Function#bind / #call / #apply +// +// The flag is read off the module the CALL is written in, never off the module that +// declares `bind` (the library) and never as one answer for the whole project. +// +// The receivers are TYPED through a method's return, the shape that made the regime +// decide at all: `factory.get(k).bind(r)`. + +export type Handler = (req: string) => string; + +export class HandlerFactory { + get(key: string): Handler { + return (req: string) => key + req; + } +} + +export function plain(value: string): string { + return value; +} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/globals.d.ts b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/globals.d.ts new file mode 100644 index 00000000..850c0f89 --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/globals.d.ts @@ -0,0 +1,66 @@ +// ============================================================================ +// The case's OWN standard library — the minimum the checker needs under `noLib`. +// ============================================================================ +// WHY noLib. This mechanism's targets are declared in the REAL `lib.es5.d.ts`, and a +// case never has it: the case oracle only records a target inside the case's own files, +// and the engine is handed only `--library `. So with the real library +// neither side can name the target and the case measures nothing. Declaring `Function` +// and `CallableFunction` HERE puts them on both sides of the comparison. +// +// This is not a mock of the behaviour. The checker resolves `CallableFunction` by NAME +// from whatever is in scope and then applies `strictBindCallApply` itself, so these +// declarations exercise the real decision. Verified: the same two declarations answer +// CallableFunction under `strict: true` and Function under +// `strict: false, strictBindCallApply: false`. +// +// Everything other than Function/CallableFunction is here only because `noLib` removes +// it and the checker requires it. Keep this file minimal: an interface added here is a +// declaration the engine can resolve to, and an accidental one is an accidental answer. + +interface Object { toString(): string; } +interface Boolean {} +interface Number {} +interface String { padStart(width: number): string; padEnd(width: number): string; trim(): string; } +interface Array { length: number; } +interface ReadonlyArray { length: number; } +interface IArguments {} +interface RegExp {} + +// ── the two declarations the flag chooses between ─────────────────────────── +// Mirrors lib.es5.d.ts: `Function` declares all three, and `CallableFunction extends +// Function` REDECLARES the same three with precise generic signatures. Both are in +// scope at every site; only the flag decides. +// NO explicit `this` parameter on these. lib.es5.d.ts declares them with one, but the +// case oracle's edge label includes a `this` parameter in the signature while the engine +// (correctly) does not treat it as a parameter at all — so `this: Function` renders as +// `Function#apply(Function,any,any)` on one side and `Function#apply(any,any)` on the +// other, and every site scores as a disagreement the engine did not actually make. +interface Function { + apply(thisArg: any, argArray?: any): any; + call(thisArg: any, ...argArray: any[]): any; + bind(thisArg: any, ...argArray: any[]): any; + readonly name: string; + toString(): string; +} + +interface CallableFunction extends Function { + // Simply typed on purpose. lib.es5.d.ts gives these precise generic signatures + // (`this: (this: T, ...args: A) => R`), and this fixture does not need them: what it + // tests is that TWO declarations of the same member names exist and that + // `strictBindCallApply` picks between them, which the checker decides from the flag + // and not from how well a signature fits. A function-typed parameter also renders + // badly in the case oracle's edge labels — the label splits on the commas inside + // `(this: T, ...args: A) => R` and emits `apply(args: A) =,T,T)`, which can never + // match the engine's label, so every site scored as a disagreement. + // + // Arguments are optional and returns are `any` so every call form in the case source + // typechecks. The two declarations stay distinguishable without help: an edge label + // carries the OWNER, so `CallableFunction#call` and `Function#call` never collide, and + // that owner is exactly what the flag decides. + call(thisArg: any, arg0?: any, arg1?: any): any; + apply(thisArg: any, args?: any): any; + bind(thisArg: any, arg0?: any): any; +} + + +interface NewableFunction extends Function {} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/loose-site.ts b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/loose-site.ts new file mode 100644 index 00000000..ab5f80d3 --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/loose-site.ts @@ -0,0 +1,20 @@ +// Governed by src/loose/tsconfig.json (strictBindCallApply: false) -> Function's +// declarations, although the project's other program is strict. +import { HandlerFactory, plain } from '../factory'; + +export function looseSites(factory: HandlerFactory, r: object): string { + const bound = factory.get('a').bind(r); + factory.get('b').call(r, 'x'); + plain.apply(null, ['y']); + return bound('z'); +} + +// CONTROL — a type that declares its own `bind` keeps it under either regime. +interface OwnBindLoose { + (x: number): number; + bind(label: string): string; +} + +export function ownBindLoose(ob: OwnBindLoose): string { + return ob.bind('own'); +} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/tsconfig.json b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/tsconfig.json new file mode 100644 index 00000000..33a61be1 --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/loose/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": false, + "strictBindCallApply": false, + "noLib": true + }, + "include": ["**/*", "../globals.d.ts"] +} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/strict-site.ts b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/strict-site.ts new file mode 100644 index 00000000..6776b405 --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/strict-site.ts @@ -0,0 +1,19 @@ +// Governed by src/tsconfig.json (strict) -> CallableFunction's declarations. +import { HandlerFactory, plain } from './factory'; + +export function strictSites(factory: HandlerFactory, r: object): string { + const bound = factory.get('a').bind(r); + factory.get('b').call(r, 'x'); + plain.apply(null, ['y']); + return bound('z'); +} + +// CONTROL — a type that declares its own `bind` keeps it under either regime. +interface OwnBind { + (x: number): number; + bind(label: string): string; +} + +export function ownBindStrict(ob: OwnBind): string { + return ob.bind('own'); +} diff --git a/graph/test/typescript/cases/82-mixed-bind-call-apply/src/tsconfig.json b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/tsconfig.json new file mode 100644 index 00000000..f901b84b --- /dev/null +++ b/graph/test/typescript/cases/82-mixed-bind-call-apply/src/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "noLib": true + }, + "exclude": ["loose"] +} diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts new file mode 100644 index 00000000..7bd87348 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts @@ -0,0 +1,14 @@ +// The PRODUCTION program's standard library (noLib, as case 21 explains). `Array.map` here +// and `Pipeline.map` below both take one callback. +interface Array { + map(fn: (v: T) => U): U[]; +} +interface Object { toString(): string; } +interface Boolean {} +interface Number {} +interface String {} +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/pipeline.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/pipeline.ts new file mode 100644 index 00000000..6c9601e8 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/pipeline.ts @@ -0,0 +1,16 @@ +// Production code, in its own program. `Pipeline` has a `map`, so by shape it "satisfies" +// the FIXTURE program's hand-written `Array` too — but no value of it can ever reach a +// receiver typed by that `Array`: neither program sees the other's files. So the fixture's +// `rows.map(...)` must not dispatch here, and `this.items.map(project)` must not reach the +// fixture's callback through that dispatch. +export class Pipeline { + constructor(private readonly items: T[]) {} + + map(project: (value: T) => U): Pipeline { + return new Pipeline(this.items.map(project)); + } +} + +export function build(xs: string[]): Pipeline { + return new Pipeline(xs).map((s) => s); +} diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json new file mode 100644 index 00000000..c901fced --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts new file mode 100644 index 00000000..745c7321 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts @@ -0,0 +1,16 @@ +// A TEST FIXTURE's own standard library, in a SIBLING program: these globals belong to +// ./tsconfig.json alone. The parameter name differs from ../app/globals.d.ts on purpose, +// so the two `Array.map` declarations are told apart in the goldens, and this header is +// one line longer so neither `Array` spans the line of the other's `map`. +interface Array { + map(each: (item: T) => U): U[]; +} +interface Object { toString(radix?: number): string; } +interface Boolean {} +interface Number {} +interface String {} +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/tsconfig.json b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/tsconfig.json new file mode 100644 index 00000000..c901fced --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts new file mode 100644 index 00000000..e0b82e39 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts @@ -0,0 +1,16 @@ +// CONTROL: `Bag` lives in this program, has the fixture `Array`'s one member and is +// constructed, so it stays a structural conformer of this program's `Array`: `rows.map` +// keeps its dispatch to `Bag.map`. Only the other program's `Pipeline` drops out. +export class Row { + key(): string { return "row"; } +} + +export class Bag { + map(each: (item: T) => U): U[] { return []; } +} + +export function keys(rows: Row[]): string[] { + return rows.map((r) => r.key()); +} + +export const bag = new Bag(); diff --git a/graph/test/typescript/cases/83-closed-world-dispatch/src/handlers.ts b/graph/test/typescript/cases/83-closed-world-dispatch/src/handlers.ts new file mode 100644 index 00000000..864290cb --- /dev/null +++ b/graph/test/typescript/cases/83-closed-world-dispatch/src/handlers.ts @@ -0,0 +1,55 @@ +// A call through an interface-typed receiver fans to every declared implementor by +// default. `--closed-world on` narrows that fan to what the program can build (#473). +// Each class below is one shape of "can the program build it": + +export interface Handler { + handle(x: string): string; +} + +// constructed with `new`: stays in the narrowed fan +export class Built implements Handler { + handle(x: string): string { return "b" + x; } +} + +// never written in a `new`, but handed to a registry as a class VALUE that constructs it: +// stays, because a class that escapes as a value can be built where the parser cannot see +export class Registered implements Handler { + handle(x: string): string { return "r" + x; } +} + +// named only as a TYPE: nothing can build it, so the narrowed fan drops it +export class Unused implements Handler { + handle(x: string): string { return "u" + x; } +} + +// never constructed itself, but a constructed subclass inherits its body: stays +export abstract class Base implements Handler { + handle(x: string): string { return "base" + x; } +} +export class Leaf extends Base {} + +// a subclass nobody constructs, overriding: dropped +export class Orphan extends Base { + handle(x: string): string { return "o" + x; } +} + +type HandlerClass = new () => Handler; +const registry: HandlerClass[] = []; +export function register(c: HandlerClass): void { registry.push(c); } +register(Registered); + +export function dispatch(h: Handler, x: string): string { + return h.handle(x); +} + +export function main(): void { + const built = new Built(); + const leaf = new Leaf(); + let later: Unused | undefined; + let other: Orphan | undefined; + dispatch(built, "1"); + dispatch(leaf, "2"); + for (const C of registry) dispatch(new C(), "3"); + void later; + void other; +} diff --git a/graph/test/typescript/cases/83-closed-world-dispatch/src/impl.ts b/graph/test/typescript/cases/83-closed-world-dispatch/src/impl.ts new file mode 100644 index 00000000..eaedd82a --- /dev/null +++ b/graph/test/typescript/cases/83-closed-world-dispatch/src/impl.ts @@ -0,0 +1,12 @@ +import { Handler } from "./handlers"; + +// declared in one module, handed to a container as a class VALUE from another +// (`useClass: Provided` in module.ts): stays in the narrowed fan +export class Provided implements Handler { + handle(x: string): string { return "p" + x; } +} + +// imported by module.ts too, but only ever named there as a TYPE: dropped +export class TypeOnly implements Handler { + handle(x: string): string { return "t" + x; } +} diff --git a/graph/test/typescript/cases/83-closed-world-dispatch/src/module.ts b/graph/test/typescript/cases/83-closed-world-dispatch/src/module.ts new file mode 100644 index 00000000..fda3351a --- /dev/null +++ b/graph/test/typescript/cases/83-closed-world-dispatch/src/module.ts @@ -0,0 +1,7 @@ +import { Provided, TypeOnly } from "./impl"; + +// The container shape: the class crosses a module boundary as an imported value and is +// constructed by code the analysis does not see. +export const providers = [{ provide: "handler", useClass: Provided }]; + +export let pending: TypeOnly | undefined; diff --git a/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts b/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts new file mode 100644 index 00000000..e462aa74 --- /dev/null +++ b/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts @@ -0,0 +1,17 @@ +class Repo { append(x: number): number { return x; } } +class Other { append(x: number): number { return x; } } +interface Ctx { repo: Repo } + +// A declared `this` parameter is what `this` is inside the function, whoever binds it. +function onEvent(this: Ctx, e: number) { return this.repo.append(e); } +function onShape(this: { other: Other }, e: number) { return this.other.append(e); } +const h = onEvent.bind({ repo: new Repo() }); +const s = onShape.bind({ other: new Other() }); + +// controls: an untyped `this` stays unknown; a class member's `this` is still its class +function onUntyped(this: any, e: number) { return this.repo.append(e); } +class Owner { + repo = new Repo(); + run(e: number) { return this.repo.append(e); } +} +export { h, s, onUntyped, Owner }; diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json new file mode 100644 index 00000000..d23a4a6c --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json @@ -0,0 +1 @@ +{ "name": "@x/app", "private": true, "dependencies": { "@x/lib": "workspace:*", "tools": "workspace:*", "zod": "^3.0.0" } } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts new file mode 100644 index 00000000..e21076ca --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts @@ -0,0 +1 @@ +export function local(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts new file mode 100644 index 00000000..74be7308 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts @@ -0,0 +1,25 @@ +import { f, Bus, K } from '@x/lib'; +import { g } from '@x/lib/sub'; +import { isOn } from '@x/lib/feature/flags'; +import { tool } from 'tools'; +import { deep } from 'tools/deep'; +// Control: a third-party package no workspace declares stays a library import. +import { z } from 'zod'; + +import { local } from './local'; + +export class S { + constructor(private bus: Bus) {} + + run(): void { + f(); + g(); + this.bus.publish(K); + if (isOn('x')) { + tool(); + } + deep(); + local(); + z.string(); + } +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts new file mode 100644 index 00000000..0e1757b6 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts @@ -0,0 +1,4 @@ +// A third-party package, declared the way its own types would declare it. +declare module 'zod' { + export const z: { string(): unknown }; +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/package.json new file mode 100644 index 00000000..43c63464 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/package.json @@ -0,0 +1 @@ +{ "name": "ws-root", "private": true } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json new file mode 100644 index 00000000..aa2d2aff --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@x/lib", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./sub": { "types": "./dist/sub/index.d.ts", "default": "./dist/sub/index.js" }, + "./feature/*": { "types": "./dist/feature/*.d.ts", "default": "./dist/feature/*.js" } + } +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts new file mode 100644 index 00000000..817e02d5 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts @@ -0,0 +1,3 @@ +export function isOn(name: string): boolean { + return name !== ''; +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts new file mode 100644 index 00000000..87c7fa1a --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts @@ -0,0 +1,7 @@ +export function f(): void {} + +export class Bus { + publish(key: string): void {} +} + +export const K = 'k'; diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts new file mode 100644 index 00000000..41f7f92b --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts @@ -0,0 +1 @@ +export function g(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json new file mode 100644 index 00000000..92a5cc83 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json @@ -0,0 +1 @@ +{ "name": "tools", "main": "dist/index.js" } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts new file mode 100644 index 00000000..50bbcc3f --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts @@ -0,0 +1 @@ +export function deep(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts new file mode 100644 index 00000000..d53b1d0a --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts @@ -0,0 +1 @@ +export function tool(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml b/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml new file mode 100644 index 00000000..4e708bd3 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'packages/*' + - 'apps/*' diff --git a/graph/test/typescript/cases/86-keyed-callback-registry/src/app.ts b/graph/test/typescript/cases/86-keyed-callback-registry/src/app.ts new file mode 100644 index 00000000..b4a0adae --- /dev/null +++ b/graph/test/typescript/cases/86-keyed-callback-registry/src/app.ts @@ -0,0 +1,68 @@ +import { ANY, Bus, MiniBus } from './bus'; +import { TOPIC } from './topics'; + +export const bus = new Bus(); + +export function onCreated(p: unknown): void { + console.log('created', p); +} +export function onRemoved(p: unknown): void { + console.log('removed', p); +} +export function onAnything(p: unknown): void { + console.log('any', p); +} + +export class Indexer { + constructor(private readonly b: Bus) {} + + start(): void { + this.b.subscribeAll({ + [TOPIC.created]: (p) => this.indexed(p), + [TOPIC.removed]: (p) => this.dropped(p), + }); + } + + indexed(p: unknown): void { + console.log('indexed', p); + } + dropped(p: unknown): void { + console.log('dropped', p); + } +} + +bus.subscribe(TOPIC.created, onCreated); +bus.subscribe('item.removed', onRemoved); +bus.subscribe(ANY, onAnything); +new Indexer(bus).start(); + +export function create(): void { + bus.publish(TOPIC.created, { id: 1 }); +} +export function remove(): void { + bus.publish('item.removed', { id: 1 }); +} +// the name is only known at run time: every handler of the registry may run +export function relay(evt: { type: string }): void { + bus.publish(evt.type, evt); +} +export function size(): number { + return bus.count(TOPIC.created); +} + +const mini = new MiniBus(); +export function onA(): void { + console.log('a'); +} +export function onAll(): void { + console.log('all'); +} +export function onB(): void { + console.log('b'); +} +mini.on('a', onA); +mini.on('*', onAll); +mini.on('b', onB); +export function go(): void { + mini.fire('a', 1); +} diff --git a/graph/test/typescript/cases/86-keyed-callback-registry/src/bus.ts b/graph/test/typescript/cases/86-keyed-callback-registry/src/bus.ts new file mode 100644 index 00000000..208bd4bc --- /dev/null +++ b/graph/test/typescript/cases/86-keyed-callback-registry/src/bus.ts @@ -0,0 +1,57 @@ +export const ANY = '*'; + +export type Handler = (payload: unknown) => void; + +// A registry of callbacks keyed by name: a Map from the name to a Set of handlers. +export class Bus { + private readonly handlers = new Map>(); + + subscribe(name: string, fn: Handler): () => void { + let set = this.handlers.get(name); + if (!set) { + set = new Set(); + this.handlers.set(name, set); + } + set.add(fn); + return () => { + set.delete(fn); + }; + } + + // every entry of the table is subscribed under its own key + subscribeAll(table: Record): void { + for (const name of Object.keys(table)) { + const fn = table[name]; + if (fn) this.subscribe(name, fn); + } + } + + // reads the registry by key and never calls what it finds + count(name: string): number { + return this.handlers.get(name)?.size ?? 0; + } + + publish(name: string, payload: unknown): void { + const run = () => this.dispatch(name, payload); + run(); + } + + private dispatch(name: string, payload: unknown): void { + const targets: Handler[] = [...(this.handlers.get(name) ?? []), ...(this.handlers.get(ANY) ?? [])]; + for (const handler of targets) handler(payload); + } +} + +// The same idea written tersely: the set created inline, and the lookup iterated directly. +export class MiniBus { + private h = new Map void>>(); + + on(k: string, fn: (p: unknown) => void) { + (this.h.get(k) ?? this.h.set(k, new Set()).get(k)!).add(fn); + } + + fire(k: string, p: unknown) { + for (const f of this.h.get(k) ?? []) f(p); + this.h.get('*')?.forEach((f) => f(p)); + } +} diff --git a/graph/test/typescript/cases/86-keyed-callback-registry/src/topics.ts b/graph/test/typescript/cases/86-keyed-callback-registry/src/topics.ts new file mode 100644 index 00000000..4a2fc759 --- /dev/null +++ b/graph/test/typescript/cases/86-keyed-callback-registry/src/topics.ts @@ -0,0 +1,4 @@ +export const TOPIC = { + created: 'item.created', + removed: 'item.removed', +} as const; 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/03-structural-satisfaction.edges b/graph/test/typescript/expected/03-structural-satisfaction.edges index f5857b6e..46193873 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.edges +++ b/graph/test/typescript/expected/03-structural-satisfaction.edges @@ -1,9 +1,23 @@ ambiguous_unknown FUNCTION_CALL duck#useLibrary() @L51 -> - +known_edge CONSTRUCTOR_CALL bus#() @L9 -> Bus#() +known_edge CONSTRUCTOR_CALL declared#() @L12 -> Declared#() known_edge CONSTRUCTOR_CALL duck#drive() @L36 -> FileReader#() known_edge CONSTRUCTOR_CALL duck#drive() @L37 -> NetReader#() known_edge CONSTRUCTOR_CALL duck#useLibrary() @L46 -> FileReader#() +known_edge CONSTRUCTOR_CALL duck-pub#() @L13 -> Hidden#() +known_edge CONSTRUCTOR_CALL duck-pub#wire() @L16 -> Duck#() +known_edge CONSTRUCTOR_CALL duck-pub#wire() @L17 -> Relay#(Pub) +known_edge CONSTRUCTOR_CALL pub#start() @L26 -> Real#() +known_edge CONSTRUCTOR_CALL pub#start() @L26 -> Relay#(Pub) +known_edge CONSTRUCTOR_CALL stranger#() @L7 -> Stranger#() known_edge FUNCTION_CALL duck#drive() @L39 -> duck#consume(Reader) known_edge METHOD_CALL duck#useLibrary() @L50 -> FileReader#close() +known_edge METHOD_CALL duck-pub#wire() @L17 -> Relay#run() +known_edge METHOD_CALL pub#start() @L26 -> Relay#run() +multi_inferred METHOD_CALL Relay#run() @L17 -> Declared#publish({ id: string }) +multi_inferred METHOD_CALL Relay#run() @L17 -> Duck#publish({ id: string },string) +multi_inferred METHOD_CALL Relay#run() @L17 -> Pub#publish({ id: string }) +multi_inferred METHOD_CALL Relay#run() @L17 -> Real#publish({ id: string }) multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> FileReader#read() multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> NetReader#read() multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> Reader#read() diff --git a/graph/test/typescript/expected/03-structural-satisfaction.entries b/graph/test/typescript/expected/03-structural-satisfaction.entries new file mode 100644 index 00000000..1dc8130f --- /dev/null +++ b/graph/test/typescript/expected/03-structural-satisfaction.entries @@ -0,0 +1,10 @@ +── entry_point (9) ── + exported_from_entry_module duck#consume duck.ts:31 + exported_from_entry_module duck#drive duck.ts:35 + exported_from_entry_module duck#useLibrary duck.ts:45 + exported_from_entry_module duck-pub#wire duck-pub.ts:15 + unimported_module bus# bus.ts:1 + unimported_module declared# declared.ts:1 + unimported_module duck# duck.ts:1 + unimported_module duck-pub# duck-pub.ts:1 + unimported_module stranger# stranger.ts:1 diff --git a/graph/test/typescript/expected/03-structural-satisfaction.envelope b/graph/test/typescript/expected/03-structural-satisfaction.envelope index 224a7991..e5fbd8c4 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.envelope +++ b/graph/test/typescript/expected/03-structural-satisfaction.envelope @@ -1,2 +1,5 @@ +nominal pub#Pub.publish -> pub#Real.publish structural duck#Reader.read -> duck#FileReader.read structural duck#Reader.read -> duck#NetReader.read +structural pub#Pub.publish -> declared#Declared.publish +structural pub#Pub.publish -> duck-pub#Duck.publish diff --git a/graph/test/typescript/expected/03-structural-satisfaction.fields b/graph/test/typescript/expected/03-structural-satisfaction.fields index e69de29b..7cdc0dfc 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.fields +++ b/graph/test/typescript/expected/03-structural-satisfaction.fields @@ -0,0 +1 @@ +known_edge read Relay#run() -> Relay#p diff --git a/graph/test/typescript/expected/03-structural-satisfaction.fields-oracle b/graph/test/typescript/expected/03-structural-satisfaction.fields-oracle index 05a32ce6..54cda29c 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.fields-oracle +++ b/graph/test/typescript/expected/03-structural-satisfaction.fields-oracle @@ -1,7 +1,7 @@ 03-structural-satisfaction [fields] - precision 0.0000 (0 correct, 0 wrong) - recall 0.0000 (0 of 0 the compiler resolved) - sites 0 resolved 0 - tiers - access + 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 diff --git a/graph/test/typescript/expected/03-structural-satisfaction.lib.edges b/graph/test/typescript/expected/03-structural-satisfaction.lib.edges index 258f2417..ac4f8304 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.lib.edges +++ b/graph/test/typescript/expected/03-structural-satisfaction.lib.edges @@ -1,9 +1,23 @@ boundary_lib FUNCTION_CALL duck#useLibrary() @L51 -> io#drain({ read(): string }) boundary_lib METHOD_CALL duck#useLibrary() @L50 -> Closeable#close() +known_edge CONSTRUCTOR_CALL bus#() @L9 -> Bus#() +known_edge CONSTRUCTOR_CALL declared#() @L12 -> Declared#() known_edge CONSTRUCTOR_CALL duck#drive() @L36 -> FileReader#() known_edge CONSTRUCTOR_CALL duck#drive() @L37 -> NetReader#() known_edge CONSTRUCTOR_CALL duck#useLibrary() @L46 -> FileReader#() +known_edge CONSTRUCTOR_CALL duck-pub#() @L13 -> Hidden#() +known_edge CONSTRUCTOR_CALL duck-pub#wire() @L16 -> Duck#() +known_edge CONSTRUCTOR_CALL duck-pub#wire() @L17 -> Relay#(Pub) +known_edge CONSTRUCTOR_CALL pub#start() @L26 -> Real#() +known_edge CONSTRUCTOR_CALL pub#start() @L26 -> Relay#(Pub) +known_edge CONSTRUCTOR_CALL stranger#() @L7 -> Stranger#() known_edge FUNCTION_CALL duck#drive() @L39 -> duck#consume(Reader) +known_edge METHOD_CALL duck-pub#wire() @L17 -> Relay#run() +known_edge METHOD_CALL pub#start() @L26 -> Relay#run() +multi_inferred METHOD_CALL Relay#run() @L17 -> Declared#publish({ id: string }) +multi_inferred METHOD_CALL Relay#run() @L17 -> Duck#publish({ id: string },string) +multi_inferred METHOD_CALL Relay#run() @L17 -> Pub#publish({ id: string }) +multi_inferred METHOD_CALL Relay#run() @L17 -> Real#publish({ id: string }) multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> FileReader#read() multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> NetReader#read() multi_inferred METHOD_CALL duck#consume(Reader) @L32 -> Reader#read() diff --git a/graph/test/typescript/expected/03-structural-satisfaction.lib.envelope b/graph/test/typescript/expected/03-structural-satisfaction.lib.envelope index 595d139a..620432e5 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.lib.envelope +++ b/graph/test/typescript/expected/03-structural-satisfaction.lib.envelope @@ -1,3 +1,6 @@ +nominal pub#Pub.publish -> pub#Real.publish structural duck#Reader.read -> duck#FileReader.read structural duck#Reader.read -> duck#NetReader.read structural lib:io#Closeable.close -> duck#FileReader.close +structural pub#Pub.publish -> declared#Declared.publish +structural pub#Pub.publish -> duck-pub#Duck.publish diff --git a/graph/test/typescript/expected/03-structural-satisfaction.lib.oracle b/graph/test/typescript/expected/03-structural-satisfaction.lib.oracle index 3716c5f0..e196b805 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.lib.oracle +++ b/graph/test/typescript/expected/03-structural-satisfaction.lib.oracle @@ -1,4 +1,7 @@ -oracle=7 engine=10 agree=7 missing=0 (known 0, NEW 0) extra=3 +oracle=18 engine=24 agree=18 missing=0 (known 0, NEW 0) extra=6 + extra Relay#run() -> Declared#publish({ id: string }) + extra Relay#run() -> Duck#publish({ id: string },string) + extra Relay#run() -> Real#publish({ id: string }) extra duck#consume(Reader) -> FileReader#read() extra duck#consume(Reader) -> NetReader#read() extra duck#useLibrary() -> FileReader#close() diff --git a/graph/test/typescript/expected/03-structural-satisfaction.oracle b/graph/test/typescript/expected/03-structural-satisfaction.oracle index 110a730c..7546b16d 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.oracle +++ b/graph/test/typescript/expected/03-structural-satisfaction.oracle @@ -1,4 +1,7 @@ -oracle=5 engine=8 agree=5 missing=0 (known 0, NEW 0) extra=3 +oracle=16 engine=22 agree=16 missing=0 (known 0, NEW 0) extra=6 + extra Relay#run() -> Declared#publish({ id: string }) + extra Relay#run() -> Duck#publish({ id: string },string) + extra Relay#run() -> Real#publish({ id: string }) extra duck#consume(Reader) -> FileReader#read() extra duck#consume(Reader) -> NetReader#read() extra duck#useLibrary() -> FileReader#close() diff --git a/graph/test/typescript/expected/03-structural-satisfaction.type-use b/graph/test/typescript/expected/03-structural-satisfaction.type-use index 83c409c6..04be63a8 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.type-use +++ b/graph/test/typescript/expected/03-structural-satisfaction.type-use @@ -1,4 +1,17 @@ +ambiguous_unknown IMPLEMENTS_INTERFACE 0 Declared [HERITAGE] -> - ambiguous_unknown VARIABLE_TYPE 0 duck [VARIABLE] -> - +known_edge IMPLEMENTS_INTERFACE 0 Real [HERITAGE] -> Pub +known_edge METHOD_PARAM 0 Relay [METHOD_PARAM] -> Pub known_edge METHOD_PARAM 0 duck [METHOD_PARAM] -> Reader +known_edge OBJECT_CREATION_TYPE 0 bus [EXPRESSION] -> Bus +known_edge OBJECT_CREATION_TYPE 0 declared [EXPRESSION] -> Declared known_edge OBJECT_CREATION_TYPE 0 duck [EXPRESSION] -> FileReader known_edge OBJECT_CREATION_TYPE 0 duck [EXPRESSION] -> NetReader +known_edge OBJECT_CREATION_TYPE 0 duck-pub [EXPRESSION] -> Duck +known_edge OBJECT_CREATION_TYPE 0 duck-pub [EXPRESSION] -> Hidden +known_edge OBJECT_CREATION_TYPE 0 duck-pub [EXPRESSION] -> Relay +known_edge OBJECT_CREATION_TYPE 0 pub [EXPRESSION] -> Real +known_edge OBJECT_CREATION_TYPE 0 pub [EXPRESSION] -> Relay +known_edge OBJECT_CREATION_TYPE 0 stranger [EXPRESSION] -> Stranger +known_edge TYPE_ALIAS_RHS 0 Seen [TYPE] -> Pub +known_edge VARIABLE_TYPE 0 duck-pub [VARIABLE] -> Pub diff --git a/graph/test/typescript/expected/03-structural-satisfaction.types-oracle b/graph/test/typescript/expected/03-structural-satisfaction.types-oracle index de06659f..d2852d66 100644 --- a/graph/test/typescript/expected/03-structural-satisfaction.types-oracle +++ b/graph/test/typescript/expected/03-structural-satisfaction.types-oracle @@ -1,7 +1,7 @@ 03-structural-satisfaction [types] - precision 1.0000 (3 correct, 0 wrong) - recall 1.0000 (3 of 3 the compiler resolved) - sites 5 resolved 4 (80.0%) - tiers ambiguous_unknown=1 known_edge=4 - contexts METHOD_PARAM=1 OBJECT_CREATION_TYPE=3 VARIABLE_TYPE=1 - not scored: 1 rows whose target is not a client declaration + precision 1.0000 (15 correct, 0 wrong) + recall 1.0000 (15 of 15 the compiler resolved) + sites 18 resolved 16 (88.9%) + tiers ambiguous_unknown=2 known_edge=16 + contexts IMPLEMENTS_INTERFACE=2 METHOD_PARAM=2 OBJECT_CREATION_TYPE=11 TYPE_ALIAS_RHS=1 VARIABLE_TYPE=2 + not scored: 2 rows whose target is not a client declaration 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.edges b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges index 4c371265..7159e17d 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges @@ -1,14 +1,31 @@ ambient_terminal FUNCTION_CALL app#atStartup() @L38 -> app#onReady(() =) +ambient_terminal FUNCTION_CALL plugin#() @L12 -> plugin#wrap(T) +ambient_terminal FUNCTION_CALL plugin#() @L7 -> plugin#wrap(T) +ambient_terminal FUNCTION_CALL store#deferred() @L24 -> store#later(() =) +ambiguous_unknown CONSTRUCTOR_CALL store#() @L8 -> - +ambiguous_unknown CONSTRUCTOR_CALL store#() @L9 -> - +ambiguous_unknown METHOD_CALL store#known() @L17 -> - +ambiguous_unknown METHOD_CALL store#store() @L12 -> - +ambiguous_unknown METHOD_CALL store#store() @L13 -> - callback_registered FUNCTION_CALL app#atStartup() @L38 -> app#() +callback_registered FUNCTION_CALL plugin#() @L7 -> plugin#() +callback_registered FUNCTION_CALL store#deferred() @L24 -> store#onUpdated() +callback_registered METHOD_CALL app#boot() @L57 -> plugin#() +callback_registered METHOD_CALL app#bootBare() @L62 -> app#() callback_registered METHOD_CALL app#doubled(number[]) @L16 -> app#(?) callback_registered METHOD_CALL app#named(number[]) @L25 -> app#double(number) callback_registered METHOD_CALL app#scaled(number[]) @L12 -> app#(?) callback_registered METHOD_CALL app#viaLocal(number[]) @L31 -> app#log(number) known_edge FUNCTION_CALL app#() @L38 -> app#record(number) known_edge FUNCTION_CALL app#(?) @L48 -> app#record(number) +known_edge FUNCTION_CALL app#() @L62 -> plugin#migrate() known_edge FUNCTION_CALL app#each(number[],(n: number) =) @L44 -> app#(number) known_edge FUNCTION_CALL app#log(number) @L30 -> app#record(number) known_edge FUNCTION_CALL app#useEach(number[]) @L48 -> app#each(number[],(n: number) =) +known_edge FUNCTION_CALL plugin#() @L8 -> plugin#migrate() +known_edge METHOD_CALL app#boot() @L57 -> app#register(unknown) +known_edge METHOD_CALL app#bootBare() @L62 -> app#register(unknown) +known_edge METHOD_CALL app#bootSettings() @L67 -> app#register(unknown) known_edge METHOD_CALL app#doubled(number[]) @L16 -> Array#map((v: T) =) known_edge METHOD_CALL app#named(number[]) @L25 -> Array#map((v: T) =) known_edge METHOD_CALL app#scaled(number[]) @L12 -> Array#map((v: T) =) diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries index 5c3d7421..314db3cf 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries @@ -1,10 +1,17 @@ -── entry_point (9) ── +── entry_point (16) ── exported_from_entry_module app#atStartup app.ts:37 + exported_from_entry_module app#boot app.ts:56 + exported_from_entry_module app#bootBare app.ts:61 + exported_from_entry_module app#bootSettings app.ts:66 exported_from_entry_module app#doubled app.ts:15 exported_from_entry_module app#each app.ts:43 exported_from_entry_module app#named app.ts:24 exported_from_entry_module app#scaled app.ts:11 exported_from_entry_module app#useEach app.ts:47 exported_from_entry_module app#viaLocal app.ts:29 + exported_from_entry_module store#deferred store.ts:23 + exported_from_entry_module store#known store.ts:16 + exported_from_entry_module store#store store.ts:11 unimported_module app# app.ts:1 unimported_module globals# globals.d.ts:1 + unimported_module store# store.ts:1 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/78-hof-callback-at-library-boundary.oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle index f31a2b29..593d7a41 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle @@ -1 +1 @@ -oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0 +oracle=17 engine=17 agree=17 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use index 960a82a1..93f4487a 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use @@ -1,2 +1,4 @@ +ambiguous_unknown OBJECT_CREATION_TYPE 0 store [EXPRESSION] -> - +ambiguous_unknown VARIABLE_TYPE 0 store [VARIABLE] -> - known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle index e7a4988d..543823f0 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle @@ -1,7 +1,7 @@ 78-hof-callback-at-library-boundary [types] 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 - contexts SUPER_TYPE=2 - not scored: 0 rows whose target is not a client declaration + sites 5 resolved 2 (40.0%) + tiers ambiguous_unknown=3 known_edge=2 + contexts OBJECT_CREATION_TYPE=2 SUPER_TYPE=2 VARIABLE_TYPE=1 + not scored: 3 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/82-mixed-bind-call-apply.edges b/graph/test/typescript/expected/82-mixed-bind-call-apply.edges new file mode 100644 index 00000000..2c0fac84 --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.edges @@ -0,0 +1,14 @@ +ambiguous_unknown FUNCTION_CALL loose/loose-site#looseSites(HandlerFactory,object) @L9 -> - +ambiguous_unknown FUNCTION_CALL strict-site#strictSites(HandlerFactory,object) @L8 -> - +known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L6 -> Function#bind(any,any[]) +known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L6 -> HandlerFactory#get(string) +known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L7 -> Function#call(any,any[]) +known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L7 -> HandlerFactory#get(string) +known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L8 -> Function#apply(any,any) +known_edge METHOD_CALL loose/loose-site#ownBindLoose(OwnBindLoose) @L19 -> OwnBindLoose#bind(string) +known_edge METHOD_CALL strict-site#ownBindStrict(OwnBind) @L18 -> OwnBind#bind(string) +known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L5 -> CallableFunction#bind(any,any) +known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L5 -> HandlerFactory#get(string) +known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L6 -> CallableFunction#call(any,any,any) +known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L6 -> HandlerFactory#get(string) +known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L7 -> CallableFunction#apply(any,any) diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.entries b/graph/test/typescript/expected/82-mixed-bind-call-apply.entries new file mode 100644 index 00000000..ff9be6b1 --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.entries @@ -0,0 +1,8 @@ +── entry_point (7) ── + exported_from_entry_module loose/loose-site#looseSites loose-site.ts:5 + exported_from_entry_module loose/loose-site#ownBindLoose loose-site.ts:18 + exported_from_entry_module strict-site#ownBindStrict strict-site.ts:17 + exported_from_entry_module strict-site#strictSites strict-site.ts:4 + unimported_module globals# globals.d.ts:1 + unimported_module loose/loose-site# loose-site.ts:1 + unimported_module strict-site# strict-site.ts:1 diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.envelope b/graph/test/typescript/expected/82-mixed-bind-call-apply.envelope new file mode 100644 index 00000000..65b5ab00 --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.envelope @@ -0,0 +1,4 @@ +nominal globals#Function.apply -> globals#CallableFunction.apply +nominal globals#Function.bind -> globals#CallableFunction.bind +nominal globals#Function.call -> globals#CallableFunction.call +value factory#@18:23 -> factory#HandlerFactory. diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.fields b/graph/test/typescript/expected/82-mixed-bind-call-apply.fields new file mode 100644 index 00000000..e69de29b diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.fields-oracle b/graph/test/typescript/expected/82-mixed-bind-call-apply.fields-oracle new file mode 100644 index 00000000..7431a689 --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.fields-oracle @@ -0,0 +1,7 @@ +82-mixed-bind-call-apply [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 diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.oracle b/graph/test/typescript/expected/82-mixed-bind-call-apply.oracle new file mode 100644 index 00000000..f31a2b29 --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.oracle @@ -0,0 +1 @@ +oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.type-use b/graph/test/typescript/expected/82-mixed-bind-call-apply.type-use new file mode 100644 index 00000000..6eaa852a --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.type-use @@ -0,0 +1,7 @@ +known_edge METHOD_PARAM 0 loose/loose-site [METHOD_PARAM] -> HandlerFactory +known_edge METHOD_PARAM 0 loose/loose-site [METHOD_PARAM] -> OwnBindLoose +known_edge METHOD_PARAM 0 strict-site [METHOD_PARAM] -> HandlerFactory +known_edge METHOD_PARAM 0 strict-site [METHOD_PARAM] -> OwnBind +known_edge METHOD_RETURN 0 HandlerFactory [METHOD] -> Handler +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/82-mixed-bind-call-apply.types-oracle b/graph/test/typescript/expected/82-mixed-bind-call-apply.types-oracle new file mode 100644 index 00000000..255041ba --- /dev/null +++ b/graph/test/typescript/expected/82-mixed-bind-call-apply.types-oracle @@ -0,0 +1,7 @@ +82-mixed-bind-call-apply [types] + precision 1.0000 (7 correct, 0 wrong) + recall 1.0000 (7 of 7 the compiler resolved) + sites 7 resolved 7 (100.0%) + tiers known_edge=7 + contexts METHOD_PARAM=4 METHOD_RETURN=1 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.edges b/graph/test/typescript/expected/82-structural-conformer-across-programs.edges new file mode 100644 index 00000000..5d1aad27 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.edges @@ -0,0 +1,9 @@ +known_edge CONSTRUCTOR_CALL Pipeline#map((value: T) =) @L10 -> Pipeline#(T[]) +known_edge CONSTRUCTOR_CALL app/pipeline#build(string[]) @L15 -> Pipeline#(T[]) +known_edge CONSTRUCTOR_CALL fixture/use#() @L16 -> Bag#() +known_edge METHOD_CALL app/pipeline#build(string[]) @L15 -> Pipeline#map((value: T) =) +known_edge METHOD_CALL fixture/use#(?) @L13 -> Row#key() +multi_inferred METHOD_CALL Pipeline#map((value: T) =) @L10 -> Array#map((v: T) =) +multi_inferred METHOD_CALL Pipeline#map((value: T) =) @L10 -> Pipeline#map((value: T) =) +multi_inferred METHOD_CALL fixture/use#keys(Row[]) @L13 -> Array#map((item: T) =) +multi_inferred METHOD_CALL fixture/use#keys(Row[]) @L13 -> Bag#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.entries b/graph/test/typescript/expected/82-structural-conformer-across-programs.entries new file mode 100644 index 00000000..b002c567 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.entries @@ -0,0 +1,7 @@ +── entry_point (6) ── + exported_from_entry_module app/pipeline#build pipeline.ts:14 + exported_from_entry_module fixture/use#keys use.ts:12 + unimported_module app/globals# globals.d.ts:1 + unimported_module app/pipeline# pipeline.ts:1 + unimported_module fixture/globals# globals.d.ts:1 + unimported_module fixture/use# use.ts:1 diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope b/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope new file mode 100644 index 00000000..bcd93261 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope @@ -0,0 +1,6 @@ +structural app/globals#Array.map -> app/pipeline#Pipeline.map +structural fixture/globals#Array.map -> fixture/use#Bag.map +value app/globals#@4:14 -> app/pipeline# +value app/pipeline#@9:19 -> app/pipeline# +value fixture/globals#@6:16 -> fixture/use# +value fixture/use#@9:16 -> fixture/use# diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.fields b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields new file mode 100644 index 00000000..a069adc8 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields @@ -0,0 +1 @@ +known_edge read Pipeline#map((value: T) =) -> Pipeline#items diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle new file mode 100644 index 00000000..ca4ac9b8 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle @@ -0,0 +1,7 @@ +82-structural-conformer-across-programs [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 diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing b/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing new file mode 100644 index 00000000..0b193f75 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing @@ -0,0 +1,5 @@ +# The per-case oracle compiles every file under src/ as ONE program (tsc-program.mjs walks +# the directory), so the two programs' `interface Array` merge and the fixture's later +# `map` overload is the one it picks. In the real layout app/ and fixture/ are separate +# programs and `this.items.map` resolves to app/globals.d.ts, which is what the engine says. +Pipeline#map((value: T) =) -> Array#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle new file mode 100644 index 00000000..f5b1660d --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle @@ -0,0 +1,5 @@ +oracle=7 engine=9 agree=6 missing=1 (known 1, NEW 0) extra=3 + known Pipeline#map((value: T) =) -> Array#map((item: T) =) + extra Pipeline#map((value: T) =) -> Array#map((v: T) =) + extra Pipeline#map((value: T) =) -> Pipeline#map((value: T) =) + extra fixture/use#keys(Row[]) -> Bag#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use b/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use new file mode 100644 index 00000000..b39a992e --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use @@ -0,0 +1,9 @@ +known_edge METHOD_RETURN 0 Pipeline [METHOD] -> Pipeline +known_edge METHOD_RETURN 0 app/pipeline [METHOD] -> Pipeline +known_edge METHOD_TYPE_ARGUMENT 0 fixture/use [EXPRESSION] -> Row +known_edge OBJECT_CREATION_TYPE 0 Pipeline [EXPRESSION] -> Pipeline +known_edge OBJECT_CREATION_TYPE 0 app/pipeline [EXPRESSION] -> Pipeline +known_edge OBJECT_CREATION_TYPE 0 fixture/use [EXPRESSION] -> Bag +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ELEMENT 1 fixture/use [METHOD_PARAM] -> Row diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle new file mode 100644 index 00000000..e1b1ff22 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle @@ -0,0 +1,7 @@ +82-structural-conformer-across-programs [types] + precision 1.0000 (6 correct, 0 wrong) + recall 1.0000 (6 of 6 the compiler resolved) + sites 11 resolved 11 (100.0%) + tiers known_edge=11 + contexts METHOD_RETURN=2 METHOD_TYPE_ARGUMENT=1 OBJECT_CREATION_TYPE=3 SUPER_TYPE=4 TYPE_ELEMENT=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.edges b/graph/test/typescript/expected/83-closed-world-dispatch.edges new file mode 100644 index 00000000..3e35361f --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.edges @@ -0,0 +1,16 @@ +ambiguous_unknown CONSTRUCTOR_CALL handlers#main() @L52 -> - +ambiguous_unknown METHOD_CALL handlers#register(HandlerClass) @L38 -> - +known_edge CONSTRUCTOR_CALL handlers#main() @L46 -> Built#() +known_edge CONSTRUCTOR_CALL handlers#main() @L47 -> Base#() +known_edge FUNCTION_CALL handlers#() @L39 -> handlers#register(HandlerClass) +known_edge FUNCTION_CALL handlers#main() @L50 -> handlers#dispatch(Handler,string) +known_edge FUNCTION_CALL handlers#main() @L51 -> handlers#dispatch(Handler,string) +known_edge FUNCTION_CALL handlers#main() @L52 -> handlers#dispatch(Handler,string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Base#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Built#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Handler#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Orphan#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Provided#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Registered#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> TypeOnly#handle(string) +multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Unused#handle(string) diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.entries b/graph/test/typescript/expected/83-closed-world-dispatch.entries new file mode 100644 index 00000000..37921bcc --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.entries @@ -0,0 +1,2 @@ +── entry_point (1) ── + unimported_module module# module.ts:1 diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.envelope b/graph/test/typescript/expected/83-closed-world-dispatch.envelope new file mode 100644 index 00000000..d8c90f4f --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.envelope @@ -0,0 +1,8 @@ +nominal handlers#Base.handle -> handlers#Orphan.handle +nominal handlers#Handler.handle -> handlers#Base.handle +nominal handlers#Handler.handle -> handlers#Built.handle +nominal handlers#Handler.handle -> handlers#Orphan.handle +nominal handlers#Handler.handle -> handlers#Registered.handle +nominal handlers#Handler.handle -> handlers#Unused.handle +nominal handlers#Handler.handle -> impl#Provided.handle +nominal handlers#Handler.handle -> impl#TypeOnly.handle diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.fields b/graph/test/typescript/expected/83-closed-world-dispatch.fields new file mode 100644 index 00000000..aa725fb8 --- /dev/null +++ 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 new file mode 100644 index 00000000..966f7948 --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle @@ -0,0 +1,7 @@ +83-closed-world-dispatch [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + 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/83-closed-world-dispatch.known-missing b/graph/test/typescript/expected/83-closed-world-dispatch.known-missing new file mode 100644 index 00000000..15bb2695 --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.known-missing @@ -0,0 +1,7 @@ +# Accepted gaps for 83-closed-world-dispatch — each line is an edge the TypeScript compiler +# resolves and this engine does not. A NEW missing edge fails the suite; +# a line here that STARTS working also fails, so the debt cannot rot. +# `new C()` where C is typed by a construct-signature alias: the compiler names the +# alias's signature, the engine leaves the site ambiguous_unknown. Unrelated to the +# closed-world fan this case is about. +handlers#main() -> handlers#() diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.oracle b/graph/test/typescript/expected/83-closed-world-dispatch.oracle new file mode 100644 index 00000000..7f00d858 --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.oracle @@ -0,0 +1,9 @@ +oracle=6 engine=12 agree=5 missing=1 (known 1, NEW 0) extra=7 + known handlers#main() -> handlers#() + extra handlers#dispatch(Handler,string) -> Base#handle(string) + extra handlers#dispatch(Handler,string) -> Built#handle(string) + extra handlers#dispatch(Handler,string) -> Orphan#handle(string) + extra handlers#dispatch(Handler,string) -> Provided#handle(string) + extra handlers#dispatch(Handler,string) -> Registered#handle(string) + extra handlers#dispatch(Handler,string) -> TypeOnly#handle(string) + extra handlers#dispatch(Handler,string) -> Unused#handle(string) diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.type-use b/graph/test/typescript/expected/83-closed-world-dispatch.type-use new file mode 100644 index 00000000..b398f762 --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.type-use @@ -0,0 +1,18 @@ +ambiguous_unknown OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> - +known_edge IMPLEMENTS_INTERFACE 0 Base [HERITAGE] -> Handler +known_edge IMPLEMENTS_INTERFACE 0 Built [HERITAGE] -> Handler +known_edge IMPLEMENTS_INTERFACE 0 Provided [HERITAGE] -> Handler +known_edge IMPLEMENTS_INTERFACE 0 Registered [HERITAGE] -> Handler +known_edge IMPLEMENTS_INTERFACE 0 TypeOnly [HERITAGE] -> Handler +known_edge IMPLEMENTS_INTERFACE 0 Unused [HERITAGE] -> Handler +known_edge METHOD_PARAM 0 handlers [METHOD_PARAM] -> Handler +known_edge METHOD_PARAM 0 handlers [METHOD_PARAM] -> HandlerClass +known_edge METHOD_RETURN 1 HandlerClass [TYPE] -> Handler +known_edge OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> Built +known_edge OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> Leaf +known_edge SUPER_TYPE 0 Leaf [HERITAGE] -> Base +known_edge SUPER_TYPE 0 Orphan [HERITAGE] -> Base +known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> HandlerClass +known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> Orphan +known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> Unused +known_edge TYPE_ELEMENT 1 module [VARIABLE] -> TypeOnly diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.types-oracle b/graph/test/typescript/expected/83-closed-world-dispatch.types-oracle new file mode 100644 index 00000000..9f55c300 --- /dev/null +++ b/graph/test/typescript/expected/83-closed-world-dispatch.types-oracle @@ -0,0 +1,7 @@ +83-closed-world-dispatch [types] + precision 1.0000 (16 correct, 0 wrong) + recall 1.0000 (16 of 16 the compiler resolved) + sites 18 resolved 17 (94.4%) + tiers ambiguous_unknown=1 known_edge=17 + contexts IMPLEMENTS_INTERFACE=6 METHOD_PARAM=2 METHOD_RETURN=1 OBJECT_CREATION_TYPE=3 SUPER_TYPE=2 TYPE_ELEMENT=4 + not scored: 1 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/84-declared-this-parameter.edges b/graph/test/typescript/expected/84-declared-this-parameter.edges new file mode 100644 index 00000000..184ea7af --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.edges @@ -0,0 +1,9 @@ +ambiguous_unknown METHOD_CALL x#() @L8 -> - +ambiguous_unknown METHOD_CALL x#() @L9 -> - +ambiguous_unknown METHOD_CALL x#onUntyped(number) @L12 -> - +known_edge CONSTRUCTOR_CALL x#() @L14 -> Repo#() +known_edge CONSTRUCTOR_CALL x#() @L8 -> Repo#() +known_edge CONSTRUCTOR_CALL x#() @L9 -> Other#() +known_edge METHOD_CALL Owner#run(number) @L15 -> Repo#append(number) +known_edge METHOD_CALL x#onEvent(number) @L6 -> Repo#append(number) +known_edge METHOD_CALL x#onShape(number) @L7 -> Other#append(number) diff --git a/graph/test/typescript/expected/84-declared-this-parameter.entries b/graph/test/typescript/expected/84-declared-this-parameter.entries new file mode 100644 index 00000000..8e41e4b4 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.entries @@ -0,0 +1,3 @@ +── entry_point (2) ── + exported_from_entry_module x#onUntyped x.ts:12 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/84-declared-this-parameter.fields b/graph/test/typescript/expected/84-declared-this-parameter.fields new file mode 100644 index 00000000..de3e89d9 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.fields @@ -0,0 +1,4 @@ +ambiguous_unknown read x#onUntyped(number) -> - +known_edge read Owner#run(number) -> Owner#repo +known_edge read x#onEvent(number) -> Ctx#repo +known_edge read x#onShape(number) -> { other: Other }#other diff --git a/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle b/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle new file mode 100644 index 00000000..0a3c5b6f --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle @@ -0,0 +1,9 @@ +84-declared-this-parameter [fields] + precision 0.6667 (2 correct, 1 wrong) + recall 0.6667 (2 of 3 the compiler resolved) + sites 4 resolved 3 (75.0%) + tiers ambiguous_unknown=1 known_edge=3 + access read=4 + not scored: 1 rows whose target is not a client declaration + WRONG x#onShape(number) READ { other: Other }#other + MISSING x#onShape(number) READ x#other diff --git a/graph/test/typescript/expected/84-declared-this-parameter.oracle b/graph/test/typescript/expected/84-declared-this-parameter.oracle new file mode 100644 index 00000000..6c4bb5cb --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.oracle @@ -0,0 +1 @@ +oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/84-declared-this-parameter.type-use b/graph/test/typescript/expected/84-declared-this-parameter.type-use new file mode 100644 index 00000000..f183c671 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.type-use @@ -0,0 +1,6 @@ +known_edge FIELD_TYPE 0 Ctx [FIELD] -> Repo +known_edge FIELD_TYPE 1 x [METHOD_PARAM] -> Other +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Ctx +known_edge OBJECT_CREATION_TYPE 0 Owner [EXPRESSION] -> Repo +known_edge OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> Other +known_edge OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> Repo diff --git a/graph/test/typescript/expected/84-declared-this-parameter.types-oracle b/graph/test/typescript/expected/84-declared-this-parameter.types-oracle new file mode 100644 index 00000000..d0200fc9 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.types-oracle @@ -0,0 +1,7 @@ +84-declared-this-parameter [types] + precision 1.0000 (5 correct, 0 wrong) + recall 1.0000 (5 of 5 the compiler resolved) + sites 6 resolved 6 (100.0%) + tiers known_edge=6 + contexts FIELD_TYPE=2 METHOD_PARAM=1 OBJECT_CREATION_TYPE=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/85-workspace-package-import.edges b/graph/test/typescript/expected/85-workspace-package-import.edges new file mode 100644 index 00000000..d8ac1ca7 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.edges @@ -0,0 +1,8 @@ +known_edge FUNCTION_CALL S#run() @L15 -> packages/lib/src/index#f() +known_edge FUNCTION_CALL S#run() @L16 -> packages/lib/src/sub/index#g() +known_edge FUNCTION_CALL S#run() @L18 -> packages/lib/src/feature/flags#isOn(string) +known_edge FUNCTION_CALL S#run() @L19 -> packages/tools/src/index#tool() +known_edge FUNCTION_CALL S#run() @L21 -> packages/tools/src/deep#deep() +known_edge FUNCTION_CALL S#run() @L22 -> apps/app/src/local#local() +known_edge METHOD_CALL S#run() @L17 -> Bus#publish(string) +known_edge METHOD_CALL S#run() @L23 -> apps/app/types/zod#string() diff --git a/graph/test/typescript/expected/85-workspace-package-import.entries b/graph/test/typescript/expected/85-workspace-package-import.entries new file mode 100644 index 00000000..79185187 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.entries @@ -0,0 +1,3 @@ +── entry_point (2) ── + unimported_module apps/app/src/main# main.ts:1 + unimported_module apps/app/types/zod# zod.ts:1 diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields b/graph/test/typescript/expected/85-workspace-package-import.fields new file mode 100644 index 00000000..be580e37 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.fields @@ -0,0 +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 new file mode 100644 index 00000000..346e6652 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle @@ -0,0 +1,7 @@ +85-workspace-package-import [fields] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + 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/85-workspace-package-import.oracle b/graph/test/typescript/expected/85-workspace-package-import.oracle new file mode 100644 index 00000000..00f772ed --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.oracle @@ -0,0 +1 @@ +oracle=8 engine=8 agree=8 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/85-workspace-package-import.type-use b/graph/test/typescript/expected/85-workspace-package-import.type-use new file mode 100644 index 00000000..28bd1b65 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.type-use @@ -0,0 +1 @@ +known_edge METHOD_PARAM 0 S [METHOD_PARAM] -> Bus diff --git a/graph/test/typescript/expected/85-workspace-package-import.types-oracle b/graph/test/typescript/expected/85-workspace-package-import.types-oracle new file mode 100644 index 00000000..ef6d2236 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.types-oracle @@ -0,0 +1,7 @@ +85-workspace-package-import [types] + 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 + contexts METHOD_PARAM=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.edges b/graph/test/typescript/expected/86-keyed-callback-registry.edges new file mode 100644 index 00000000..38603009 --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.edges @@ -0,0 +1,60 @@ +ambiguous_unknown CONSTRUCTOR_CALL Bus#subscribe(string,Handler) @L12 -> - +ambiguous_unknown CONSTRUCTOR_CALL MiniBus#on(string,(p: unknown) =) @L50 -> - +ambiguous_unknown CONSTRUCTOR_CALL bus#() @L47 -> - +ambiguous_unknown CONSTRUCTOR_CALL bus#() @L7 -> - +ambiguous_unknown FUNCTION_CALL MiniBus#(?) @L55 -> - +ambiguous_unknown FUNCTION_CALL MiniBus#fire(string,unknown) @L54 -> - +ambiguous_unknown METHOD_CALL Bus#() @L17 -> - +ambiguous_unknown METHOD_CALL Bus#count(string) @L31 -> - +ambiguous_unknown METHOD_CALL Bus#dispatch(string,unknown) @L40 -> - +ambiguous_unknown METHOD_CALL Bus#subscribe(string,Handler) @L10 -> - +ambiguous_unknown METHOD_CALL Bus#subscribe(string,Handler) @L13 -> - +ambiguous_unknown METHOD_CALL Bus#subscribe(string,Handler) @L15 -> - +ambiguous_unknown METHOD_CALL Bus#subscribeAll(Record) @L23 -> - +ambiguous_unknown METHOD_CALL Indexer#dropped(unknown) @L30 -> - +ambiguous_unknown METHOD_CALL Indexer#indexed(unknown) @L27 -> - +ambiguous_unknown METHOD_CALL MiniBus#fire(string,unknown) @L54 -> - +ambiguous_unknown METHOD_CALL MiniBus#fire(string,unknown) @L55 -> - +ambiguous_unknown METHOD_CALL MiniBus#on(string,(p: unknown) =) @L50 -> - +ambiguous_unknown METHOD_CALL app#onA() @L55 -> - +ambiguous_unknown METHOD_CALL app#onAll() @L58 -> - +ambiguous_unknown METHOD_CALL app#onAnything(unknown) @L13 -> - +ambiguous_unknown METHOD_CALL app#onB() @L61 -> - +ambiguous_unknown METHOD_CALL app#onCreated(unknown) @L7 -> - +ambiguous_unknown METHOD_CALL app#onRemoved(unknown) @L10 -> - +callback_registered METHOD_CALL MiniBus#fire(string,unknown) @L55 -> MiniBus#(?) +event_dispatch METHOD_CALL app#create() @L40 -> Indexer#(?) +event_dispatch METHOD_CALL app#create() @L40 -> app#onAnything(unknown) +event_dispatch METHOD_CALL app#create() @L40 -> app#onCreated(unknown) +event_dispatch METHOD_CALL app#go() @L67 -> app#onA() +event_dispatch METHOD_CALL app#go() @L67 -> app#onAll() +event_dispatch METHOD_CALL app#relay({ type: string }) @L47 -> Indexer#(?) +event_dispatch METHOD_CALL app#relay({ type: string }) @L47 -> Indexer#(?) +event_dispatch METHOD_CALL app#relay({ type: string }) @L47 -> app#onAnything(unknown) +event_dispatch METHOD_CALL app#relay({ type: string }) @L47 -> app#onCreated(unknown) +event_dispatch METHOD_CALL app#relay({ type: string }) @L47 -> app#onRemoved(unknown) +event_dispatch METHOD_CALL app#remove() @L43 -> Indexer#(?) +event_dispatch METHOD_CALL app#remove() @L43 -> app#onAnything(unknown) +event_dispatch METHOD_CALL app#remove() @L43 -> app#onRemoved(unknown) +known_edge CONSTRUCTOR_CALL app#() @L37 -> Indexer#(Bus) +known_edge CONSTRUCTOR_CALL app#() @L4 -> Bus#() +known_edge CONSTRUCTOR_CALL app#() @L53 -> MiniBus#() +known_edge FUNCTION_CALL Bus#dispatch(string,unknown) @L41 -> bus#(unknown) +known_edge FUNCTION_CALL Bus#publish(string,unknown) @L36 -> Bus#run() +known_edge METHOD_CALL Bus#run() @L35 -> Bus#dispatch(string,unknown) +known_edge METHOD_CALL Bus#subscribeAll(Record) @L25 -> Bus#subscribe(string,Handler) +known_edge METHOD_CALL Indexer#(?) @L21 -> Indexer#indexed(unknown) +known_edge METHOD_CALL Indexer#(?) @L22 -> Indexer#dropped(unknown) +known_edge METHOD_CALL Indexer#start() @L20 -> Bus#subscribeAll(Record) +known_edge METHOD_CALL app#() @L34 -> Bus#subscribe(string,Handler) +known_edge METHOD_CALL app#() @L35 -> Bus#subscribe(string,Handler) +known_edge METHOD_CALL app#() @L36 -> Bus#subscribe(string,Handler) +known_edge METHOD_CALL app#() @L37 -> Indexer#start() +known_edge METHOD_CALL app#() @L63 -> MiniBus#on(string,(p: unknown) =) +known_edge METHOD_CALL app#() @L64 -> MiniBus#on(string,(p: unknown) =) +known_edge METHOD_CALL app#() @L65 -> MiniBus#on(string,(p: unknown) =) +known_edge METHOD_CALL app#create() @L40 -> Bus#publish(string,unknown) +known_edge METHOD_CALL app#go() @L67 -> MiniBus#fire(string,unknown) +known_edge METHOD_CALL app#relay({ type: string }) @L47 -> Bus#publish(string,unknown) +known_edge METHOD_CALL app#remove() @L43 -> Bus#publish(string,unknown) +known_edge METHOD_CALL app#size() @L50 -> Bus#count(string) diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.entries b/graph/test/typescript/expected/86-keyed-callback-registry.entries new file mode 100644 index 00000000..afec440b --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.entries @@ -0,0 +1,13 @@ +── entry_point (12) ── + exported_from_entry_module app#create app.ts:39 + exported_from_entry_module app#go app.ts:66 + exported_from_entry_module app#onA app.ts:54 + exported_from_entry_module app#onAll app.ts:57 + exported_from_entry_module app#onAnything app.ts:12 + exported_from_entry_module app#onB app.ts:60 + exported_from_entry_module app#onCreated app.ts:6 + exported_from_entry_module app#onRemoved app.ts:9 + exported_from_entry_module app#relay app.ts:46 + exported_from_entry_module app#remove app.ts:42 + exported_from_entry_module app#size app.ts:49 + unimported_module app# app.ts:1 diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.envelope b/graph/test/typescript/expected/86-keyed-callback-registry.envelope new file mode 100644 index 00000000..0a65686b --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.envelope @@ -0,0 +1 @@ +value bus#@9:41 -> bus#Bus.@16 diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.fields b/graph/test/typescript/expected/86-keyed-callback-registry.fields new file mode 100644 index 00000000..7d5b88cf --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields @@ -0,0 +1,26 @@ +ambiguous_unknown read Bus#count(string) -> - +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 new file mode 100644 index 00000000..f6c67a1e --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle @@ -0,0 +1,9 @@ +86-keyed-callback-registry [fields] + 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 app#relay({ type: string }) READ app#type diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.known-missing b/graph/test/typescript/expected/86-keyed-callback-registry.known-missing new file mode 100644 index 00000000..5a8e9f31 --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.known-missing @@ -0,0 +1,10 @@ +# Accepted gaps for 86-keyed-callback-registry — each line is an edge the TypeScript compiler +# resolves and this engine does not. A NEW missing edge fails the suite; +# a line here that STARTS working also fails, so the debt cannot rot. +# +# The call through an element of a Set held in a Map resolves, for the compiler, to the element's +# function type; with no standard library staged the engine cannot type the Set's elements, so the +# call site stays unresolved (Bus.dispatch annotates its array, so its call resolves). The functions that call RUNS are reached by the keyed-registry edges +# (event_dispatch) from each publisher, which is what this case is about. +MiniBus#(?) -> MiniBus#(unknown) +MiniBus#fire(string,unknown) -> MiniBus#(unknown) diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.oracle b/graph/test/typescript/expected/86-keyed-callback-registry.oracle new file mode 100644 index 00000000..ee8df0d7 --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.oracle @@ -0,0 +1,3 @@ +oracle=20 engine=18 agree=18 missing=2 (known 2, NEW 0) extra=0 + known MiniBus#(?) -> MiniBus#(unknown) + known MiniBus#fire(string,unknown) -> MiniBus#(unknown) diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.type-use b/graph/test/typescript/expected/86-keyed-callback-registry.type-use new file mode 100644 index 00000000..1f405a87 --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.type-use @@ -0,0 +1,14 @@ +ambiguous_unknown AS_TARGET 0 topics [EXPRESSION] -> - +ambiguous_unknown METHOD_PARAM 0 Bus [METHOD_PARAM] -> - +ambiguous_unknown METHOD_TYPE_ARGUMENT 0 Bus [EXPRESSION] -> - +ambiguous_unknown METHOD_TYPE_ARGUMENT 0 MiniBus [EXPRESSION] -> - +ambiguous_unknown OBJECT_CREATION_TYPE 0 Bus [EXPRESSION] -> - +ambiguous_unknown OBJECT_CREATION_TYPE 0 MiniBus [EXPRESSION] -> - +known_edge METHOD_PARAM 0 Bus [METHOD_PARAM] -> Handler +known_edge METHOD_PARAM 0 Indexer [METHOD_PARAM] -> Bus +known_edge OBJECT_CREATION_TYPE 0 app [EXPRESSION] -> Bus +known_edge OBJECT_CREATION_TYPE 0 app [EXPRESSION] -> Indexer +known_edge OBJECT_CREATION_TYPE 0 app [EXPRESSION] -> MiniBus +known_edge TYPE_ARGUMENT 1 Bus [EXPRESSION] -> Handler +known_edge TYPE_ARGUMENT 1 Bus [METHOD_PARAM] -> Handler +known_edge TYPE_ELEMENT 1 Bus [VARIABLE] -> Handler diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.types-oracle b/graph/test/typescript/expected/86-keyed-callback-registry.types-oracle new file mode 100644 index 00000000..82d34b05 --- /dev/null +++ b/graph/test/typescript/expected/86-keyed-callback-registry.types-oracle @@ -0,0 +1,7 @@ +86-keyed-callback-registry [types] + precision 1.0000 (5 correct, 0 wrong) + recall 1.0000 (5 of 5 the compiler resolved) + sites 16 resolved 8 (50.0%) + tiers ambiguous_unknown=8 known_edge=8 + contexts AS_TARGET=1 METHOD_PARAM=3 METHOD_TYPE_ARGUMENT=2 OBJECT_CREATION_TYPE=7 TYPE_ARGUMENT=2 TYPE_ELEMENT=1 + not scored: 8 rows whose target is not a client declaration 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/ground-truth/tsc-program.mjs b/graph/test/typescript/ground-truth/tsc-program.mjs index bcf4b561..020f6614 100644 --- a/graph/test/typescript/ground-truth/tsc-program.mjs +++ b/graph/test/typescript/ground-truth/tsc-program.mjs @@ -7,7 +7,7 @@ * them is meaningless. Copying ninety lines of labelling is how they would drift apart. * The existing per-case goldens prove the lift changed nothing. * - * loadProgram(srcDir, libDir, toolName) + * loadProgram(srcDir, libDir, toolName[, programDir]) * -> { ts, program, checker, own, reachable, root, libRoot, * moduleName, simple, labelOf, callerOf, diagnostics } * @@ -18,7 +18,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { loadTypeScript } from './load-typescript.mjs'; -export function loadProgram(srcDir, libDir, toolName) { +export function loadProgram(srcDir, libDir, toolName, programDir) { const root = path.resolve(srcDir); // Through the shared loader like the rest of the stack: a bare `import ts from // 'typescript'` resolves to whatever is nearest and dies on a property access if that @@ -70,29 +70,65 @@ export function loadProgram(srcDir, libDir, toolName) { // The PARSER already reads the case's tsconfig (it must, to emit the resolved flag at // ts_module c27), so honouring it here is what makes the two sides describe the same // program. - const caseConfig = path.join(root, 'tsconfig.json'); - if (fs.existsSync(caseConfig)) { - const read = ts.readConfigFile(caseConfig, ts.sys.readFile); - if (read.error) { - console.error(`case tsconfig is unreadable: ${caseConfig}`); - process.exit(1); + // A NESTED tsconfig (`programDir`) is a second program in the same case, as the parser + // treats it: its options are merged over the case root's, so one case can hold a strict + // program and a loose one side by side (#416). The caller picks, per file, the program + // whose directory governs it. + const configDirs = [root]; + if (programDir !== undefined && path.resolve(programDir) !== root) { + configDirs.push(path.resolve(programDir)); + } + for (const configDir of configDirs) { + const caseConfig = path.join(configDir, 'tsconfig.json'); + if (fs.existsSync(caseConfig)) { + const read = ts.readConfigFile(caseConfig, ts.sys.readFile); + if (read.error) { + console.error(`case tsconfig is unreadable: ${caseConfig}`); + process.exit(1); + } + const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, configDir); + if (parsed.errors.length) { + console.error(`case tsconfig is invalid: ${caseConfig}`); + for (const e of parsed.errors) { + console.error(` ${ts.flattenDiagnosticMessageText(e.messageText, ' ')}`); + } + process.exit(1); + } + // Merged, not replaced: a case states the ONE option it is about and inherits the + // rest, so a case tsconfig cannot silently drop `lib` and change every other answer. + Object.assign(options, parsed.options); + // `noLib` and the default `lib` list contradict each other, and the default is ours, + // not the case's — so a case asking for noLib gets it rather than getting both. + if (options.noLib) delete options.lib; + // `files`/`include` are ignored on purpose — the file set is the directory walk above, + // which is what the parser is handed too. } - const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root); - if (parsed.errors.length) { - console.error(`case tsconfig is invalid: ${caseConfig}`); - for (const e of parsed.errors) { - console.error(` ${ts.flattenDiagnosticMessageText(e.messageText, ' ')}`); + } + + // ── a WORKSPACE PACKAGE below the case root, imported by its name ─────────── + // In a real monorepo the compiler reaches a sibling package through a node_modules + // link and the declarations its build wrote. A case holds neither, so the program is + // told where each named package's source is by the plainest convention there is: the + // package is `/src/index.ts`, and `/x` is `/src/x`. Deliberately not + // the parser's rule (which reads main / types / exports): the two must agree on the + // answer without sharing the reasoning. A case with no nested package.json is unchanged. + const workspacePaths = {}; + (function walk(d) { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory() && e.name !== 'node_modules') { + walk(p); + } else if (e.name === 'package.json' && d !== root) { + const name = JSON.parse(fs.readFileSync(p, 'utf-8')).name; + if (typeof name === 'string' && name !== '') { + workspacePaths[name] = [path.join(d, 'src', 'index.ts')]; + workspacePaths[`${name}/*`] = [path.join(d, 'src', '*'), path.join(d, 'src', '*', 'index.ts')]; + } } - process.exit(1); } - // Merged, not replaced: a case states the ONE option it is about and inherits the - // rest, so a case tsconfig cannot silently drop `lib` and change every other answer. - Object.assign(options, parsed.options); - // `noLib` and the default `lib` list contradict each other, and the default is ours, - // not the case's — so a case asking for noLib gets it rather than getting both. - if (options.noLib) delete options.lib; - // `files`/`include` are ignored on purpose — the file set is the directory walk above, - // which is what the parser is handed too. + })(root); + if (Object.keys(workspacePaths).length > 0) { + options.paths = { ...workspacePaths, ...(options.paths ?? {}) }; } const program = ts.createProgram(files, options); @@ -194,7 +230,10 @@ export function loadProgram(srcDir, libDir, toolName) { const mods = ts.canHaveModifiers(decl) ? (ts.getModifiers(decl) ?? []) : []; if (mods.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) name = 'static ' + name; - const ps = (decl.parameters ?? []).map((param) => { + // A declared `this` parameter types `this`, it is no argument: the engine's label + // leaves it out, so this one does too. + const ps = (decl.parameters ?? []).filter((param) => + !(ts.isIdentifier(param.name) && param.name.text === 'this')).map((param) => { let t = param.type ? param.type.getText(sf) : '?'; if (param.dotDotDotToken && !t.endsWith('[]')) t += '[]'; return simple(t); diff --git a/graph/test/typescript/run-tests.sh b/graph/test/typescript/run-tests.sh index ae67e28e..145cb86b 100755 --- a/graph/test/typescript/run-tests.sh +++ b/graph/test/typescript/run-tests.sh @@ -247,6 +247,14 @@ if ! bash "$HERE/tools/envelope-merge-test.sh"; then exit 1 fi PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}" +# ── --closed-world on narrows the dispatch fan and records it (#473) ───────── +# The goldens pin the default (CHA) fan; this solves case 82 with the flag on and checks +# that exactly the never-constructed implementors drop, each with an assumption row, and +# that a class built through `new`, through a registry, or through a subclass stays. +if ! bash "$HERE/tools/closed-world-dispatch-test.sh"; then + echo "aborting: --closed-world does not narrow the fan the way it claims" + exit 1 +fi WORK="$HERE/.work" # shellcheck source=../tools/case-pool.sh . "$ROOT/graph/test/tools/case-pool.sh" diff --git a/graph/test/typescript/tools/closed-world-dispatch-test.sh b/graph/test/typescript/tools/closed-world-dispatch-test.sh new file mode 100644 index 00000000..c7070d15 --- /dev/null +++ b/graph/test/typescript/tools/closed-world-dispatch-test.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# `--closed-world on` NARROWS THE DISPATCH FAN, AND SAYS SO (#473). +# +# By default an interface-typed call fans to every declared implementor (CHA). With the +# flag the fan is narrowed to the implementors a value the program can build could run +# (RTA), and every edge the premise removed is exported as a +# dispatch_assumes_closed_world row. The default is pinned by the ordinary golden of case +# 83-closed-world-dispatch; this solves the same case with the flag on and checks: +# +# dropped Unused (named only as a type), Orphan (a subclass nobody constructs), +# TypeOnly (imported into another module, but named there only as a type) +# KEPT Built (a `new`), Registered (handed to a registry as a class VALUE — the +# DI/factory shape), Base (never constructed, but its body runs on a Leaf), +# Provided (imported into another module and handed over as `useClass:` — +# the container shape, where the value is an import binding, not the class) +# and the assumption file names exactly the three dropped targets, while the flag-off +# run writes none. +# The four kept targets are the controls: each is a way a class is built without the +# engine seeing a typed `new` of it, and losing any one would be a wrong narrowing. +# ───────────────────────────────────────────────────────────────────────────── +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +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 +PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}" +CASE="$HERE/../cases/83-closed-world-dispatch" + +fail=0; checks=0 +ok(){ checks=$((checks+1)); return 0; } +bad(){ checks=$((checks+1)); printf ' FAIL %s\n' "$1"; fail=1; } + +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +mkdir -p "$W/ir" "$W/empty" +if ! node "$PARSER" "$CASE/src" cw false "$W/ir" >"$W/parse.log" 2>&1; then + echo " FAIL parse — see the log below"; sed 's/^/ /' "$W/parse.log" | tail -20 + echo "closed-world-dispatch: FAILED"; exit 1 +fi +for mode in off on; do + if ! bash "$ROOT/graph/pipeline/run-souffle.sh" --debug --language typescript --closed-world "$mode" \ + --client-ir "$W/ir" --library "$W/empty" --intermediate "$W/$mode/int" --output "$W/$mode/out" \ + >"$W/$mode.log" 2>&1; then + echo " FAIL solve ($mode)"; tail -20 "$W/$mode.log" | sed 's/^/ /' + echo "closed-world-dispatch: FAILED"; exit 1 + fi + python3 "$HERE/normalize_edges.py" "$W/ir" "$W/$mode/out/raw" > "$W/$mode.edges" +done + +site='handlers#dispatch(Handler,string) @L42 -> ' +has(){ grep -qF "multi_inferred METHOD_CALL $site$2" "$W/$1.edges"; } + +kept='Built#handle(string) Registered#handle(string) Base#handle(string) Provided#handle(string)' +dropped='Unused#handle(string) Orphan#handle(string) TypeOnly#handle(string)' +for t in $kept $dropped; do + has off "$t" && ok || bad "flag off: the CHA fan lost $t — the default must not move" +done +for t in $kept; do + has on "$t" && ok || bad "flag on: $t was narrowed away, but the program can build a value that runs it" +done +for t in $dropped; do + has on "$t" && bad "flag on: $t is still in the fan, though nothing constructs it" || ok +done + +gone="$(comm -23 <(sort "$W/off.edges") <(sort "$W/on.edges") | wc -l | tr -d ' ')" +added="$(comm -13 <(sort "$W/off.edges") <(sort "$W/on.edges") | wc -l | tr -d ' ')" +[ "$gone" = 3 ] && [ "$added" = 0 ] && ok || bad "flag on moved $gone edge(s) out and $added in; expected exactly the 3 dropped fan edges" + +aoff="$W/off/out/raw/assumption-dispatch-closed-world.csv" +aon="$W/on/out/raw/assumption-dispatch-closed-world.csv" +[ -f "$aoff" ] && [ ! -s "$aoff" ] && ok || bad "flag off wrote assumption rows (or no file): the default makes no closed-world step" +n="$(wc -l < "$aon" 2>/dev/null | tr -d ' ')" +[ "${n:-0}" = 3 ] && ok || bad "flag on wrote ${n:-no} assumption row(s); expected one per dropped edge (3)" +grep -q "dispatch closed world = on" "$W/on.log" && ok || bad "the solve log does not report the closed-world mode" + +if [ "$fail" != 0 ]; then echo "closed-world-dispatch: FAILED ($checks checks)"; exit 1; fi +echo "closed-world-dispatch: ok ($checks checks)" diff --git a/graph/test/typescript/tools/normalize_edges.py b/graph/test/typescript/tools/normalize_edges.py index 291ccaa4..9807d757 100644 --- a/graph/test/typescript/tools/normalize_edges.py +++ b/graph/test/typescript/tools/normalize_edges.py @@ -283,8 +283,9 @@ def main(): if f[1] not in n.m or f[3] not in n.m: continue # a function the site HANDS OVER (`xs.map(cb)`) is not a call the compiler lists at that - # site: the pairs are scored against the compiler, the .edges golden still carries it - if f[5] == 'callback_registered': + # site, nor is the handler a publish reaches through a keyed registry (`event_dispatch`): the + # pairs are scored against the compiler, the .edges golden still carries both + if f[5] in ('callback_registered', 'event_dispatch'): continue seen.add(f"{n.label(f[1])} -> {n.label(f[3])}") else: 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/test/typescript/tools/tsc_oracle_case.mjs b/graph/test/typescript/tools/tsc_oracle_case.mjs index 681e8f48..21b0783a 100644 --- a/graph/test/typescript/tools/tsc_oracle_case.mjs +++ b/graph/test/typescript/tools/tsc_oracle_case.mjs @@ -18,83 +18,108 @@ * * usage: node tsc_oracle_case.mjs [lib-dir] */ +import fs from 'node:fs'; import path from 'node:path'; import { loadProgram } from '../ground-truth/tsc-program.mjs'; -const { - ts, program, checker, own, reachable, labelOf, callerOf, diagnostics, - implicitCtorOwner, labelOfImplicitCtor, -} = loadProgram(process.argv[2], process.argv[3], 'tsc_oracle_case'); +// ONE PROGRAM PER tsconfig. A tsconfig.json below the case root is a second program, +// exactly as the parser treats it, and a file is answered by the program whose directory +// is nearest above it — so a case can compile one directory strict and another loose +// (#416). A case with only the root tsconfig loads one program, as before. +const caseRoot = path.resolve(process.argv[2] ?? '.'); +const programDirs = [caseRoot]; +(function walk(d) { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + if (!e.isDirectory()) continue; + const sub = path.join(d, e.name); + if (fs.existsSync(path.join(sub, 'tsconfig.json'))) programDirs.push(sub); + walk(sub); + } +})(caseRoot); +const programs = programDirs.map((dir) => ({ + dir, ...loadProgram(process.argv[2], process.argv[3], 'tsc_oracle_case', dir), +})); +const governing = (file) => programs + .filter((p) => file === p.dir || file.startsWith(p.dir + path.sep)) + .reduce((a, b) => (b.dir.length > a.dir.length ? b : a)); const pairs = new Set(); -for (const sf of program.getSourceFiles()) { - if (!own.has(path.resolve(sf.fileName))) continue; - const visit = (node) => { - // A DECORATOR APPLICATION IS A CALL, and this side did not think so. The project - // oracle enumerates `ts.isDecorator` and calls it DECORATOR_CALL; this list omitted - // it, so the per-case suite was blind to decorators entirely — green on them whatever - // the engine did, while a project run counted 191 absences on a decorator-driven - // codebase. No case had ever used a decorator, so nothing caught the disagreement. - // Whichever side is right, both must say it, and the compiler settles it: it resolves - // the application, because a decorator is a function invoked with (target, key, - // descriptor). #233. - if (ts.isCallExpression(node) || ts.isNewExpression(node) - || ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node) - || ts.isTaggedTemplateExpression(node) || ts.isDecorator(node)) { - let sig; - try { sig = checker.getResolvedSignature(node); } catch { sig = undefined; } - let decl = sig?.declaration; - // AN IMPLICIT CONSTRUCTOR HAS NO DECLARATION. `new Bag()` on a class that - // declares no constructor anywhere in its chain resolves to a signature whose - // `declaration` is undefined, so the site was silently unscored — and the - // engine's answer for it, whatever it was, went unchecked. The parser now - // synthesises that constructor on the ROOT class of the `extends` chain (the - // one that extends nothing; a subclass runs its base's), so the compiler side - // names the same declaration: the root class, labelled as its ``. #583. - let target; - if (decl === undefined && ts.isNewExpression(node)) { - const cls = implicitCtorOwner(node); - target = cls === undefined ? undefined : labelOfImplicitCtor(cls); - } else { - target = labelOf(decl); +for (const { dir, ts, program, checker, own, labelOf, callerOf, + implicitCtorOwner, labelOfImplicitCtor } of programs) { + for (const sf of program.getSourceFiles()) { + const file = path.resolve(sf.fileName); + if (!own.has(file) || governing(file).dir !== dir) continue; + const visit = (node) => { + // A DECORATOR APPLICATION IS A CALL, and this side did not think so. The project + // oracle enumerates `ts.isDecorator` and calls it DECORATOR_CALL; this list omitted + // it, so the per-case suite was blind to decorators entirely — green on them whatever + // the engine did, while a project run counted 191 absences on a decorator-driven + // codebase. No case had ever used a decorator, so nothing caught the disagreement. + // Whichever side is right, both must say it, and the compiler settles it: it resolves + // the application, because a decorator is a function invoked with (target, key, + // descriptor). #233. + if (ts.isCallExpression(node) || ts.isNewExpression(node) + || ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node) + || ts.isTaggedTemplateExpression(node) || ts.isDecorator(node)) { + let sig; + try { sig = checker.getResolvedSignature(node); } catch { sig = undefined; } + let decl = sig?.declaration; + // AN IMPLICIT CONSTRUCTOR HAS NO DECLARATION. `new Bag()` on a class that + // declares no constructor anywhere in its chain resolves to a signature whose + // `declaration` is undefined, so the site was silently unscored — and the + // engine's answer for it, whatever it was, went unchecked. The parser now + // synthesises that constructor on the ROOT class of the `extends` chain (the + // one that extends nothing; a subclass runs its base's), so the compiler side + // names the same declaration: the root class, labelled as its ``. #583. + let target; + if (decl === undefined && ts.isNewExpression(node)) { + const cls = implicitCtorOwner(node); + target = cls === undefined ? undefined : labelOfImplicitCtor(cls); + } else { + target = labelOf(decl); + } + if (target !== undefined) pairs.add(`${callerOf(node)} -> ${target}`); } - if (target !== undefined) pairs.add(`${callerOf(node)} -> ${target}`); - } - // AN ACCESSOR IS INVOKED BY THE ACCESS. `c.req.url` runs `get url()` and `c.res = r` - // runs `set res(v)`, and the compiler knows which declaration each is: the symbol at - // the property name carries the get and set declarations. A read names the getter; an - // assignment target names the setter; a compound assignment or an update (`x.n += 1`, - // `x.n++`) reads then writes and names both. The engine emits these as PROPERTY_READ / - // PROPERTY_WRITE edges (#703), and without this they would be unscored extras: the - // one shape whose ground truth is the compiler's and was never asked of it. - if (ts.isPropertyAccessExpression(node)) { - let sym; - try { sym = checker.getSymbolAtLocation(node.name); } catch { sym = undefined; } - if (sym && (sym.flags & ts.SymbolFlags.Alias)) sym = checker.getAliasedSymbol(sym); - if (sym && (sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))) { - const parent = node.parent; - const isLeft = ts.isBinaryExpression(parent) && parent.left === node - && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment - && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment; - const plain = isLeft && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; - const update = (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) - && (parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken); - const reads = !plain; - const writes = isLeft || update; - for (const d of sym.declarations ?? []) { - if ((reads && ts.isGetAccessorDeclaration(d)) || (writes && ts.isSetAccessorDeclaration(d))) { - const target = labelOf(d); - if (target !== undefined) pairs.add(`${callerOf(node)} -> ${target}`); + // AN ACCESSOR IS INVOKED BY THE ACCESS. `c.req.url` runs `get url()` and `c.res = r` + // runs `set res(v)`, and the compiler knows which declaration each is: the symbol at + // the property name carries the get and set declarations. A read names the getter; an + // assignment target names the setter; a compound assignment or an update (`x.n += 1`, + // `x.n++`) reads then writes and names both. The engine emits these as PROPERTY_READ / + // PROPERTY_WRITE edges (#703), and without this they would be unscored extras: the + // one shape whose ground truth is the compiler's and was never asked of it. + if (ts.isPropertyAccessExpression(node)) { + let sym; + try { sym = checker.getSymbolAtLocation(node.name); } catch { sym = undefined; } + if (sym && (sym.flags & ts.SymbolFlags.Alias)) sym = checker.getAliasedSymbol(sym); + if (sym && (sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))) { + const parent = node.parent; + const isLeft = ts.isBinaryExpression(parent) && parent.left === node + && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment + && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment; + const plain = isLeft && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; + const update = (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) + && (parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken); + const reads = !plain; + const writes = isLeft || update; + for (const d of sym.declarations ?? []) { + if ((reads && ts.isGetAccessorDeclaration(d)) || (writes && ts.isSetAccessorDeclaration(d))) { + const target = labelOf(d); + if (target !== undefined) pairs.add(`${callerOf(node)} -> ${target}`); + } } } } - } - ts.forEachChild(node, visit); - }; - ts.forEachChild(sf, visit); + ts.forEachChild(node, visit); + }; + ts.forEachChild(sf, visit); + } } -const diags = diagnostics(); +// A diagnostic counts only in a file its own program governs: the root program also +// compiles the nested program's files under the root's options, and those are not the +// options the author wrote for them. +const diags = programs.flatMap((p) => p.diagnostics().filter((d) => !d.file + || governing(path.resolve(d.file.fileName)).dir === p.dir)); if (diags.length > 0) { // A case that does not typecheck has an unreliable oracle: the checker still // answers, but it answers about a program the author did not mean to write. @@ -102,7 +127,7 @@ if (diags.length > 0) { // case does not typecheck -- threw a ReferenceError instead of printing anything, and // the author saw a stack trace from the oracle rather than the compiler's message. // The branch only runs when a fixture fails to compile, which is why it survived. - const caseRoot = path.resolve(process.argv[2] ?? '.'); + const { ts } = programs[0]; for (const d of diags.slice(0, 8)) { // A diagnostic about the PROGRAM rather than a file -- a bad compiler option, a // missing lib -- carries no `d.file` either. 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/call-edge-generation/keyed_registry.dl b/graph/typescript/engine/call-edge-generation/keyed_registry.dl new file mode 100644 index 00000000..b1eabc1d --- /dev/null +++ b/graph/typescript/engine/call-edge-generation/keyed_registry.dl @@ -0,0 +1,267 @@ +// ============================================================================ +// CALL-EDGE-GEN · A CALLBACK KEPT IN A KEYED REGISTRY (the in-process bus) +// +// An in-process publish/subscribe is a class that keeps functions in a field keyed by a +// name and runs the ones under a name when that name is published: +// +// subscribe(name, fn) { (this.handlers.get(name) ?? …).add(fn); } +// publish(name, p) { for (const h of this.handlers.get(name) ?? []) h(p); } +// bus.subscribe('order.placed', onPlaced); … bus.publish('order.placed', o); +// +// Neither end calls the other. The call inside `publish` goes through a variable the +// registry filled, and the only rule that reached a handler from there was the function-type +// envelope (value-flow.dl, typed_holder_signature): every function ever passed where the +// handler type is expected became a candidate of that type's signature, so a publish on one +// name reached the handlers of every name, and of every other registry typed the same way. +// +// What ties the two ends together is the KEY both callers write. So the join is made between +// the CALLERS of the two methods, as the messaging rules join a producer to a consumer +// (framework-behavior/destinations.dl), but in process and on the project's own class: +// +// * a STORE: a method puts its function parameter into a field under its key parameter — +// `slot.add(fn)` / `push` / `unshift` on the slot `this.F.get(k)` or `this.F[k]` (directly, +// through `??`, `!`, `as`, or a variable it initialises), `this.F.set(k, fn)`, `this.F[k] = fn`; +// * a DISPATCH: a method reads the same field under its key parameter and CALLS what it gets — +// `for (const h of slot)`, a spread of the slot into an array it iterates, `slot.forEach(h => h())`. +// A read that calls nothing (`this.F.get(k)?.size`) is not a dispatch; +// * each is followed through wrappers that pass their own parameter on (`publish` forwarding the +// name to a private `dispatch`, from inside a closure too), and a method that subscribes each +// entry of an object parameter under its own key (`subscribeAll({ [K]: fn })`) registers every +// entry of the literal its callers pass; +// * a key is a string literal, a const holding one, or a member of a const object literal +// (`as const` too), local or imported. A key known only at run time matches every key. A constant +// key the dispatch ALSO looks up (`this.F.get('*')`) makes a registration under it match every +// publish. Not destinations.dl's rd_val, which reads more: that reader is fed by call_chain_edge +// (its wrapper hop) and negates inside, so an edge here built on it could not be stratified. +// +// The edge runs from the PUBLISH call site to the registered function, tier event_dispatch: the +// site does not call the handler, the registry does, as the JavaScript engine records an emitter's +// `emit` reaching an `on` handler. Receivers are not told apart: two instances of one bus class +// share its registrations, the precision the class gives. +// +// And the envelope row that made every handler a candidate of the handler type is withdrawn for the +// store's function parameter, but only where the registry has a dispatch this file recognised — +// otherwise it is the only route there is. +// ============================================================================ + +.decl kr_param(c0:symbol, c1:symbol, c2:symbol) +.decl kr_read(c0:symbol, c1:symbol, c2:symbol) +.decl kr_yields(c0:symbol, c1:symbol) +.decl kr_slot(c0:symbol, c1:symbol, c2:symbol) +.decl kr_adds(c0:symbol) +.decl kr_store(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +.decl kr_elem_holder(c0:symbol, c1:symbol, c2:symbol) +.decl kr_runs(c0:symbol, c1:symbol, c2:symbol) +.decl kr_runs_param(c0:symbol, c1:symbol, c2:symbol) +.decl kr_dispatch_lit(c0:symbol, c1:symbol, c2:symbol) +.decl kr_dispatched(c0:symbol) +.decl kr_site_runs(c0:symbol, c1:symbol) +.decl kr_disp(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +.decl kr_reg(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +.decl kr_table_elem(c0:symbol, c1:symbol, c2:symbol) +.decl kr_table(c0:symbol, c1:symbol, c2:symbol) +.decl kr_key_arg(c0:symbol) +.decl kr_keyed(c0:symbol) +.decl kr_keyval(c0:symbol, c1:symbol) +.decl kr_fn(c0:symbol, c1:symbol) +.decl kr_forwards(c0:symbol) +.decl kr_registers(c0:symbol, c1:symbol, c2:symbol) +.decl kr_table_lit(c0:symbol) +.decl kr_at(c0:symbol, c1:number, c2:number) +.decl kr_entry(c0:symbol, c1:symbol, c2:symbol) +.decl kr_static_key(c0:symbol, c1:symbol) +.decl kr_computed_root(c0:symbol, c1:symbol) +.decl kr_key_candidate(c0:symbol, c1:symbol, c2:number) +.decl kr_entry_key(c0:symbol, c1:symbol) +.decl kr_dispatch_site(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol) +.decl kr_match(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +.decl keyed_dispatch(c0:symbol, c1:symbol) +.decl kr_registry_param(c0:symbol) + +// an expression that names a parameter: (expr, the function declaring it, its position) +kr_param(e, owner, pos) :- expr_referenced("client", "PARAMETER", p, e), param_decl("client", _, pos, _, owner, p). + +// ── the registry, read under a key: `this.F.get(k)`, `this.F[k]` ───────────── +kr_read(ce, f, k) :- call_site("client", _, "get", _, recv, ce, _), recv != "", ts_field_access_target(recv, f), + expr_child("client", ce, "ARGUMENT", "0", k). +kr_read(e, f, k) :- expr_kind("client", "ELEMENT_ACCESS", _, e), expr_child("client", e, "RECEIVER", _, r), + ts_field_access_target(r, f), expr_child("client", e, "INDEX_ARGUMENT", _, k). + +// kr_yields(X, Read): iterating or adding to X reaches the collection Read found: X is the read, +// an operand `??` / `||` / `&&` may yield, a transparent wrapper, or an array spreading it +kr_yields(e, e) :- kr_read(e, _, _). +kr_yields(p, e) :- kr_yields(c, e), expr_child("client", p, r, _, c), expr_kind("client", "BINARY_EXPRESSION", _, p), + expr_operator("client", op, p), binary_op_yields_operand(op, r). +kr_yields(p, e) :- kr_yields(c, e), expr_child("client", p, r, _, c), expr_kind("client", k, _, p), + expr_kind_is_transparent(k), edge_role_is_operand(r). +kr_yields(p, e) :- kr_yields(c, e), expr_child("client", p, "SPREAD_OPERAND", _, c), expr_kind("client", "SPREAD_ELEMENT", _, p). +kr_yields(p, e) :- kr_yields(c, e), expr_kind("client", "SPREAD_ELEMENT", _, c), expr_child("client", p, "ARRAY_ELEMENT", _, c). +// kr_slot(X, F, K): X holds the collection registry F keeps under key expression K +kr_slot(x, f, k) :- kr_yields(x, e), kr_read(e, f, k). +kr_slot(x, f, k) :- expr_referenced("client", "VARIABLE", v, x), var_initializer("client", _, i, v), i != "", + kr_slot(i, f, k). + +// ── the store: (method, key parameter position, function parameter position, field) ── +kr_adds("add"). +kr_adds("push"). +kr_adds("unshift"). +kr_store(s, kp, fp, f) :- call_site("client", _, mn, _, recv, ce, _), kr_adds(mn), recv != "", + kr_slot(recv, f, k), kr_param(k, s, kp), + expr_child("client", ce, "ARGUMENT", "0", a), kr_param(a, s, fp). +kr_store(s, kp, fp, f) :- call_site("client", _, "set", _, recv, ce, _), recv != "", ts_field_access_target(recv, f), + expr_child("client", ce, "ARGUMENT", "0", k), kr_param(k, s, kp), + expr_child("client", ce, "ARGUMENT", "1", a), kr_param(a, s, fp). +kr_store(s, kp, fp, f) :- expr_kind("client", "ASSIGNMENT_EXPRESSION", _, asg), + expr_child("client", asg, "LEFT_OPERAND", _, t), kr_read(t, f, k), kr_param(k, s, kp), + expr_child("client", asg, "RIGHT_OPERAND", _, a), kr_param(a, s, fp). + +// ── the dispatch: a method calls what the registry holds under a key ───────── +// kr_elem_holder(H, F, K): the variable or parameter H is bound to an element of the slot +kr_elem_holder(v, f, k) :- for_binding_iterable(v, it), kr_slot(it, f, k). +kr_elem_holder(p, f, k) :- call_site("client", "METHOD_CALL", n, _, recv, ce, _), recv != "", kr_slot(recv, f, k), + array_method_element_param(n, pos), expr_child("client", ce, "ARGUMENT", "0", a), + expr_anon_decl("client", cb, a), param_decl("client", _, pos, _, cb, p). +kr_elem_holder(p, f, k) :- call_site("client", "OPTIONAL_CALL", n, _, recv, ce, _), recv != "", kr_slot(recv, f, k), + array_method_element_param(n, pos), expr_child("client", ce, "ARGUMENT", "0", a), + expr_anon_decl("client", cb, a), param_decl("client", _, pos, _, cb, p). +// (method that reads the key, field, key expression) — a call is made through the element +kr_runs(d, f, k) :- kr_elem_holder(h, f, k), called_through(_, h), kr_read(e, f, k), expr_enclosing_method(e, d). +// the key is a parameter: whoever calls its function names the key +kr_runs_param(owner, pos, f) :- kr_runs(_, f, k), kr_param(k, owner, pos). +kr_dispatched(f) :- kr_runs(_, f, _). +// the key is a constant the dispatch also looks up (`this.F.get(ANY)`) +kr_demand(k) :- kr_runs(_, _, k), !kr_param(k, _, _). +kr_dispatch_lit(d, f, v) :- kr_runs(d, f, k), !kr_param(k, _, _), kr_str(k, v). + +// ── through wrappers ───────────────────────────────────────────────────────── +kr_site_runs(ce, m) :- expr_resolves_to_method(ce, m). +kr_site_runs(ce, m) :- call_runs_method(ce, m). +// kr_disp(W, Pos, F, D): calling W with a key at Pos runs, in D, what F holds under it +kr_disp(o, pos, f, d) :- kr_runs_param(o, pos, f), kr_runs(d, f, k), kr_param(k, o, pos). +kr_disp(w, pos, f, d) :- kr_disp(w2, p2, f, d), kr_site_runs(oc, w2), expr_child("client", oc, "ARGUMENT", p2, a), + kr_param(a, w, pos), w != w2. +// kr_reg(W, KeyPos, FnPos, F): calling W stores the function at FnPos in F under the key at KeyPos +kr_reg(s, kp, fp, f) :- kr_store(s, kp, fp, f). +kr_reg(w, kp2, fp2, f) :- kr_reg(s, kp, fp, f), kr_site_runs(oc, s), + expr_child("client", oc, "ARGUMENT", kp, ka), kr_param(ka, w, kp2), + expr_child("client", oc, "ARGUMENT", fp, fa), kr_param(fa, w, fp2), w != s. +// kr_table(W, Pos, F): W stores each entry of the object at Pos under the entry's key: +// the function it stores is `table[k]` of that parameter, directly or through a const +kr_table_elem(x, w, tp) :- expr_kind("client", "ELEMENT_ACCESS", _, x), expr_child("client", x, "RECEIVER", _, r), + kr_param(r, w, tp). +kr_table_elem(x, w, tp) :- expr_referenced("client", "VARIABLE", v, x), var_initializer("client", _, i, v), i != "", + kr_table_elem(i, w, tp). +kr_table(w, tp, f) :- kr_reg(s, kp, fp, f), kr_site_runs(oc, s), + expr_child("client", oc, "ARGUMENT", kp, ka), !expr_kind("client", "LITERAL", _, ka), + expr_child("client", oc, "ARGUMENT", fp, fa), kr_table_elem(fa, w, tp). + +// ── the string a key expression holds (on demand) ──────────────────────────── +.decl kr_demand(c0:symbol) +.decl kr_str(c0:symbol, c1:symbol) +.decl kr_denotes_var(c0:symbol, c1:symbol) +.decl kr_objlit(c0:symbol, c1:symbol) +.decl kr_obj(c0:symbol, c1:symbol) +.decl kr_member(c0:symbol, c1:symbol) +kr_denotes_var(e, v) :- kr_demand(e), expr_referenced("client", "VARIABLE", v, e). +kr_denotes_var(e, v) :- kr_demand(e), expr_referenced("client", "IMPORT_BINDING", ih, e), import_binds(ih, "client", "VARIABLE", v). +kr_str(e, v) :- kr_demand(e), expr_kind("client", "LITERAL", _, e), expr_literal_type("client", "STRING", e), + expr_literal_value("client", v, e). +// a const holding it +kr_demand(i) :- kr_denotes_var(_, v), var_initializer("client", _, i, v), i != "". +kr_str(e, s) :- kr_denotes_var(e, v), var_initializer("client", _, i, v), i != "", kr_str(i, s). +// `x as const`, `x!` +kr_demand(c) :- kr_demand(e), expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, r, _, c), + edge_role_is_operand(r). +kr_str(e, s) :- kr_demand(e), expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, r, _, c), + edge_role_is_operand(r), kr_str(c, s). +// `TOPIC.created` over `const TOPIC = { created: '…' } as const` +kr_objlit(i, i) :- expr_kind("client", "OBJECT_LITERAL", _, i). +kr_objlit(i, l) :- expr_kind("client", k, _, i), expr_kind_is_transparent(k), expr_child("client", i, r, _, c), + edge_role_is_operand(r), kr_objlit(c, l). +kr_obj(q, l) :- kr_demand(e), property_access_name(e, _), expr_child("client", e, "RECEIVER", _, q), + expr_referenced("client", "VARIABLE", v, q), var_initializer("client", _, i, v), kr_objlit(i, l). +kr_obj(q, l) :- kr_demand(e), property_access_name(e, _), expr_child("client", e, "RECEIVER", _, q), + expr_referenced("client", "IMPORT_BINDING", ih, q), import_binds(ih, "client", "VARIABLE", v), + var_initializer("client", _, i, v), kr_objlit(i, l). +kr_member(e, val) :- kr_demand(e), property_access_name(e, n), expr_child("client", e, "RECEIVER", _, q), kr_obj(q, l), + expr_child("client", l, "OBJECT_PROPERTY_KEY", pos, kk), expr_literal_value("client", n, kk), + expr_child("client", l, "OBJECT_PROPERTY_VALUE", pos, val). +kr_demand(val) :- kr_member(_, val). +kr_str(e, s) :- kr_member(e, val), kr_str(val, s). + +// ── the key a call writes ──────────────────────────────────────────────────── +kr_key_arg(a) :- kr_reg(s, kp, _, _), kr_site_runs(oc, s), expr_child("client", oc, "ARGUMENT", kp, a). +kr_key_arg(a) :- kr_disp(w, pos, _, _), kr_site_runs(oc, w), expr_child("client", oc, "ARGUMENT", pos, a). +kr_demand(a) :- kr_key_arg(a). +kr_keyed(a) :- kr_key_arg(a), kr_str(a, _). +kr_keyval(a, v) :- kr_key_arg(a), kr_str(a, v). +kr_keyval(a, "{}") :- kr_key_arg(a), !kr_keyed(a). + +// the function an argument hands over where it is written: a function, an arrow, an import, +// `this.onX`, `onX.bind(this)`, or a const holding one. A parameter passed on is not: the +// wrapper that passes it is followed instead (kr_reg), so its callers are the registrations. +kr_fn(x, m) :- expr_callable(x, m), !expr_kind("client", "CALL_EXPRESSION", _, x). +kr_fn(x, m) :- method_value(x, m). +kr_fn(x, m) :- expr_referenced("client", "VARIABLE", v, x), holder_holds_function(v, m), !kr_table_elem(x, _, _). +// (`const fn = table[name]` inside a wrapper that files each entry is a table hop, kr_table, not a +// registration of every entry under a key only known at run time) + +// a call inside a wrapper that passes its own parameter on is a hop of the wrapper, not a site +kr_forwards(oc) :- kr_disp(w2, p2, _, _), kr_site_runs(oc, w2), expr_child("client", oc, "ARGUMENT", p2, a), + kr_param(a, w, pos), kr_disp(w, pos, _, _). + +// ── registrations: (field, key, function) ──────────────────────────────────── +kr_registers(f, kv, m) :- kr_reg(s, kp, fp, f), kr_site_runs(rc, s), + expr_child("client", rc, "ARGUMENT", kp, ka), kr_keyval(ka, kv), + expr_child("client", rc, "ARGUMENT", fp, fa), kr_fn(fa, m). +// a table: each entry of the object literal passed (or the const it initialises) +kr_table_lit(l) :- kr_table(w, tp, _), kr_site_runs(rc, w), expr_child("client", rc, "ARGUMENT", tp, a), + value_branch_lit(a, l). +.decl value_branch_lit(c0:symbol, c1:symbol) +value_branch_lit(a, a) :- expr_kind("client", "OBJECT_LITERAL", _, a). +value_branch_lit(a, l) :- expr_referenced("client", "VARIABLE", v, a), var_initializer("client", _, l, v), + expr_kind("client", "OBJECT_LITERAL", _, l). +kr_entry(l, pos, v) :- kr_table_lit(l), expr_child("client", l, "OBJECT_PROPERTY_VALUE", pos, v). +kr_static_key(l, pos) :- kr_table_lit(l), expr_child("client", l, "OBJECT_PROPERTY_KEY", pos, _). +kr_entry_key(v, kv) :- kr_entry(l, pos, v), expr_child("client", l, "OBJECT_PROPERTY_KEY", pos, kk), + expr_literal_value("client", kv, kk). +// A COMPUTED key (`[TOPIC.created]: fn`) is its own root in the IR, with no link to its property +// (the walker reaches it as COMPUTED_PROPERTY_NAME). It is the last computed-name root of the same +// module that starts inside the literal and ends before the entry's value begins. +kr_at(e, to_number(sl) * 100000 + to_number(sc), to_number(el) * 100000 + to_number(ec)) :- + expr_location("client", sl, sc, el, ec, e), (kr_table_lit(e) ; kr_entry(_, _, e) ; kr_computed_root(e, _)). +kr_computed_root(c, mod) :- expr_root_context("client", "COMPUTED_PROPERTY_NAME", c), expr_kind("client", _, "ROOT", c), + expr_module("client", mod, c). +kr_key_candidate(v, c, cs) :- kr_entry(l, pos, v), !kr_static_key(l, pos), expr_module("client", mod, l), + kr_computed_root(c, mod), kr_at(l, ls, le), kr_at(c, cs, ce), kr_at(v, vs, _), + cs > ls, ce <= vs, vs < le. +kr_entry_key(v, kv) :- kr_key_candidate(v, c, cs), cs = max s : { kr_key_candidate(v, _, s) }, + kr_str(c, kv). +kr_demand(c) :- kr_key_candidate(_, c, _). +kr_registers(f, kv, m) :- kr_table(w, tp, f), kr_site_runs(rc, w), expr_child("client", rc, "ARGUMENT", tp, a), + value_branch_lit(a, l), kr_entry(l, _, v), kr_entry_key(v, kv), kr_fn(v, m). + +// ── the join ───────────────────────────────────────────────────────────────── +kr_dispatch_site(dc, from, f, d, kv) :- kr_disp(w, pos, f, d), kr_site_runs(dc, w), !kr_forwards(dc), + expr_child("client", dc, "ARGUMENT", pos, a), kr_keyval(a, kv), call_from(dc, from). +kr_match(kd, kr, f, d) :- kr_dispatch_site(_, _, f, d, kd), kr_registers(f, kr, _), kd = kr. +kr_match(kd, kr, f, d) :- kr_dispatch_site(_, _, f, d, kd), kr_registers(f, kr, _), kd = "{}". +kr_match(kd, kr, f, d) :- kr_dispatch_site(_, _, f, d, kd), kr_registers(f, kr, _), kr = "{}". +kr_match(kd, kr, f, d) :- kr_dispatch_site(_, _, f, d, kd), kr_registers(f, kr, _), kr_dispatch_lit(d, f, kr). +keyed_dispatch(dc, m) :- kr_dispatch_site(dc, _, f, d, kd), kr_registers(f, kr, m), kr_match(kd, kr, f, d). + +call_chain_edge(dc, caller, "-", m, "client", "event_dispatch", kind) :- keyed_dispatch(dc, m), + method_prov(m, "client"), call_from(dc, caller), caller != m, invocation_site(dc, kind). + +// the store's function parameter, in a registry this file dispatches: the envelope row for it +// is withdrawn (value-flow.dl, typed_holder_signature) — on the method, on each overload signature +// a call selects instead of it, and on a wrapper that passes its own parameter on +kr_registry_param(p) :- kr_reg(s, _, fp, f), kr_dispatched(f), param_decl("client", _, fp, _, s, p). +kr_registry_param(p) :- kr_reg(s, _, fp, f), kr_dispatched(f), group_implementation(g, s), method_group_of(sig, g), + param_decl("client", _, fp, _, sig, p). + +// A registry's slot is a collection: `slot.add(fn)`, `slot.delete(fn)` store or drop the function and +// never call it, as for a Map or Set the types name (value-flow.dl, collection_store_site). Without +// the library staged the slot has no type, and every function ever subscribed was "handed" to the +// store and reached from every caller of subscribe. +collection_store_site(ce) :- call_site("client", _, mn, _, recv, ce, _), recv != "", mn != "forEach", kr_slot(recv, _, _). 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/config-resolution/knobs.dl b/graph/typescript/engine/config-resolution/knobs.dl index 3e093b7a..c9064f72 100644 --- a/graph/typescript/engine/config-resolution/knobs.dl +++ b/graph/typescript/engine/config-resolution/knobs.dl @@ -202,3 +202,53 @@ ts_http_client_module("axios"). ts_http_client_factory("create"). .decl ts_http_base_url_key(c0:symbol) ts_http_base_url_key("baseURL"). + +// ── MESSAGING: a key both ends spell (a topic, a queue, a message pattern) ── +// ts_msg_client_type(Specifier, TypeName, Role): a receiver DECLARED as TypeName, +// imported from Specifier, sends or subscribes. Matched on the declared type and the +// package it comes from, never on the method name alone: `emit` on a Node event +// emitter, `send` on an HTTP reply or a socket, `subscribe` on an observable are none +// of these. +// nest_client `.emit(key, data)` / `.send(key, data)` +// kafka_producer `.send({ topic, messages })` +// kafka_consumer `.subscribe({ topic })` / `({ topics: [...] })` +.decl ts_msg_client_type(c0:symbol, c1:symbol, c2:symbol) +ts_msg_client_type("@nestjs/microservices", "ClientProxy", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientKafka", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientRMQ", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientNats", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientRedis", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientMqtt", "nest_client"). +ts_msg_client_type("@nestjs/microservices", "ClientTCP", "nest_client"). +ts_msg_client_type("kafkajs", "Producer", "kafka_producer"). +ts_msg_client_type("kafkajs", "Consumer", "kafka_consumer"). +// ts_msg_send(Role, Method, Transport): the call that sends; the key is argument 0, +// or its `topic` property when the argument is a record (ts_msg_record_key) +.decl ts_msg_send(c0:symbol, c1:symbol, c2:symbol) +ts_msg_send("nest_client", "emit", "message"). +ts_msg_send("nest_client", "send", "message"). +ts_msg_send("kafka_producer", "send", "kafka"). +.decl ts_msg_subscribe(c0:symbol, c1:symbol, c2:symbol) +ts_msg_subscribe("kafka_consumer", "subscribe", "kafka"). +// the property of a record argument that names the key, and the one naming several +.decl ts_msg_record_key(c0:symbol) +ts_msg_record_key("topic"). +.decl ts_msg_record_keys(c0:symbol) +ts_msg_record_keys("topics"). +// ts_msg_handler_decorator(Name): a method decorator whose argument 0 is the key the +// method handles (`@EventPattern(TOPICS.x)`, `@MessagePattern('orders.place')`) +.decl ts_msg_handler_decorator(c0:symbol) +ts_msg_handler_decorator("EventPattern"). +ts_msg_handler_decorator("MessagePattern"). +// ts_msg_transport_member(Written, Transport): the decorator's optional argument 1 +.decl ts_msg_transport_member(c0:symbol, c1:symbol) +ts_msg_transport_member("Transport.KAFKA", "kafka"). +ts_msg_transport_member("Transport.RMQ", "amqp"). +ts_msg_transport_member("Transport.NATS", "nats"). +ts_msg_transport_member("Transport.REDIS", "redis"). +ts_msg_transport_member("Transport.MQTT", "mqtt"). +ts_msg_transport_member("Transport.TCP", "tcp"). +// the transport of an end that does not say which broker it uses (a Nest client is +// bound to one in module configuration): it joins an end of any transport +.decl ts_remote_transport_message(c0:symbol) +ts_remote_transport_message("message"). diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index db0a0bd1..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). @@ -799,6 +795,7 @@ expr_call_dispatch(ce, m) :- call_site("client", _, cn, rk, recv, ce, _), !call_super_receiver(ce), !dispatch_too_wide(rt, cn), implementors(rt, sub), + dispatch_admits(sub), declared_method_direct(sub, cn, "false", m). // ── THE STRUCTURAL FAN ────────────────────────────────────────────────────── @@ -843,6 +840,7 @@ call_super_receiver(ce) :- call_site("client", _, _, "SUPER", _, ce, _). dispatch_width(rt, cn, n) :- implementor_member(rt, cn, _), n = count : { implementor_member(rt, cn, _) }. implementor_member(rt, cn, m) :- implementors(rt, sub), + dispatch_admits(sub), declared_method_direct(sub, cn, "false", m). implementor_member(rt, cn, m) :- structural_implementor(rt, sub), declared_method_direct(sub, cn, "false", m). @@ -851,6 +849,59 @@ dispatch_too_wide(rt, cn) :- dispatch_width(rt, cn, n), k = to_number(c), n > k. +// ── the closed-world narrowing (#473) — OPT-IN ────────────────────────────── +// By default the nominal fan is every declared override the hierarchy admits (CHA), +// bounded only by the cap. With `--closed-world on` it is narrowed to the overrides a +// value the program can build could run (RTA), which is how Python's self-dispatch is +// already bounded. The two are different claims under one tier, so the default is +// unchanged and the narrowed run says it was narrowed: `run.dispatch_closed_world`, and +// one dispatch_assumes_closed_world row per edge the premise removed. +// +// NARROWING IS SOUND ONLY IF EVERY CONSTRUCTION IS VISIBLE, and TypeScript has two +// common ways to build a class without a `new` the parser can type: a framework or +// container instantiates a class it was HANDED (`providers: [OrderService]`, +// `register(Handler)`), and a factory calls `new C()` on a class it received as a +// value. Both hand the class around as a VALUE, so a class referenced as a value +// anywhere counts as constructible, alongside every typed `new`. What is left out is a +// class that is only ever named as a TYPE, or not named at all — the case RTA exists for. +// +// A SUBTYPE IS LIVE IF A CONSTRUCTIBLE TYPE CONFORMS TO IT, not only if it is itself +// constructed: `class Mid { run() {} }` extended by a constructed `Leaf` that does not +// redeclare `run` executes Mid.run, and dropping Mid would lose an edge a run takes. The +// conformance closure (not the member-inheritance one) is used so an interface that +// only a constructed class implements stays in the fan exactly as it is today. +closed_world_dispatch() :- dispatch_closed_world("on"). +type_constructible(t) :- type_instantiated(t, _). +type_constructible(t) :- expr_referenced("client", "TYPE", t, _). +// ...and from ANOTHER module the same value is an IMPORT_BINDING, not a TYPE: a Nest +// module's `useClass: FileRelationalRepository` names the class it imported. Followed +// through import_binds directly, not expr_static_type, which sits inside the call +// resolution this narrowing feeds (dispatch_too_wide negates it: a stratification cycle). +type_constructible(t) :- expr_referenced("client", "IMPORT_BINDING", ih, _), + import_binds(ih, _, "TYPE", t). +type_live(t) :- type_constructible(t). +type_live(anc) :- type_constructible(t), type_subtype_star(t, anc). +type_live(sib) :- type_live(t), scope_sibling(t, sib). +dispatch_admits(sub) :- implementors(_, sub), !closed_world_dispatch(). +dispatch_admits(sub) :- implementors(_, sub), closed_world_dispatch(), type_live(sub). + +// ── dispatch_assumes_closed_world(CallExprHash, DroppedMethodHash) ────────── +// THE ASSUMPTION IS A ROW, NOT A FOOTNOTE — the counterpart of Python's +// self_dispatch_assumes_closed_world. Each row is an edge the CHA fan had and the +// narrowed fan does not, so a consumer can count what the premise cost and restore it +// for a library whose subclasses live in someone else's code. Empty unless the flag is on. +dispatch_assumes_closed_world(ce, m) :- closed_world_dispatch(), + call_site("client", _, cn, rk, recv, ce, _), + receiver_kind_is_value(rk), + cn != "", + expr_type(recv, _, rt), + !call_exact_receiver(ce), + !call_super_receiver(ce), + !dispatch_too_wide(rt, cn), + implementors(rt, sub), + !type_live(sub), + declared_method_direct(sub, cn, "false", m). + // ── the merged candidate set ──────────────────────────────────────────────── expr_call_candidate(ce, m) :- expr_call_dispatch(ce, m). diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index 7e0a11fc..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). @@ -169,6 +167,16 @@ expr_type(e, "client", t) :- expr_referenced("client", "THIS", _, e), expr_enclosing_type(e, t). expr_type(e, "client", t) :- expr_kind("client", "THIS_REFERENCE", _, e), expr_enclosing_type(e, t). +// A declared `this` parameter (`function onEvent(this: Ctx, e: Evt)`) is what `this` is in +// that function, whoever binds it (`onEvent.bind(ctx)`, `.call(ctx)`, a framework): the +// compiler types `this.repo` from it, and without it the site had no receiver type at all. +expr_type(e, prov, t) :- this_expr(e), expr_enclosing_method(e, m), method_declared_this(m, p), + param_type(p, prov, t). +expr_shape(e, s) :- this_expr(e), expr_enclosing_method(e, m), method_declared_this(m, p), + param_shape_target(p, s). +this_expr(e) :- expr_referenced("client", "THIS", _, e). +this_expr(e) :- expr_kind("client", "THIS_REFERENCE", _, e). +method_declared_this(m, p) :- param_shape("client", "THIS", _, _, p), param_decl("client", _, _, _, m, p). // `this` INSIDE A STATIC METHOD IS THE CLASS, so its members are the STATICS. A helper // class that calls its own statics through `this` is ordinary TypeScript — @@ -371,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), @@ -391,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). @@ -430,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). @@ -452,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). @@ -490,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), @@ -507,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). @@ -541,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/expression-resolution/overload.dl b/graph/typescript/engine/expression-resolution/overload.dl index 948d5b68..f5d28a76 100644 --- a/graph/typescript/engine/expression-resolution/overload.dl +++ b/graph/typescript/engine/expression-resolution/overload.dl @@ -91,10 +91,33 @@ call_has_spread(ce) :- call_site("client", _, _, _, _, ce, cs), // target, so its count is unknown in the same sense a spread's is: arity stands down. call_has_spread(ce) :- bare_decorator_call(ce). +// ── sbca_site_rejects(CallExpr, Callee) — the other family of call/apply/bind ── +// In a MIXED project member-lookup.dl seeds BOTH `Function` and `CallableFunction` for +// a callable receiver, because the project has no single answer. A call site does: its +// module's governing tsconfig decides (module_sbca_governed), so here the family that +// module's flag rules out is dropped. Only where the other family's same-named member +// is ALSO a candidate at the site, so a receiver that reaches `Function.bind` alone +// (an interface extending Function with no call signature) keeps it under any flag. +sbca_site_rejects(ce, m) :- expr_call_candidate(ce, m), + call_site("client", _, _, _, _, ce, cs), + call_module(cs, mod), + module_sbca_governed(mod, "true"), + plain_function_member(n, m), + callable_function_member(n, other), + expr_call_candidate(ce, other). +sbca_site_rejects(ce, m) :- expr_call_candidate(ce, m), + call_site("client", _, _, _, _, ce, cs), + call_module(cs, mod), + module_sbca_governed(mod, "false"), + callable_function_member(n, m), + plain_function_member(n, other), + expr_call_candidate(ce, other). + // ── arity_match(CallExpr, Callee) ─────────────────────────────────────────── // Guarded by the candidate set, so the numeric comparison is never a cross product // of all N-argument calls against all N-parameter methods. arity_match(ce, m) :- expr_call_candidate(ce, m), + !sbca_site_rejects(ce, m), call_expr_argc(ce, n), !call_has_spread(ce), method_min_arity(m, lo), @@ -102,6 +125,7 @@ arity_match(ce, m) :- expr_call_candidate(ce, m), method_max_arity(m, hi), n <= hi. arity_match(ce, m) :- expr_call_candidate(ce, m), + !sbca_site_rejects(ce, m), call_has_spread(ce). // AN IMPLEMENTATION'S OWN MAXIMUM IS NOT A BOUND ON THE CALL. // `const assert: (left, right) => R = x => x as any` is legal TypeScript: @@ -120,6 +144,7 @@ arity_match(ce, m) :- expr_call_candidate(ce, m), // passes fewer arguments than a parameter the callee cannot default is still not // a call to it. arity_match(ce, m) :- expr_call_candidate(ce, m), + !sbca_site_rejects(ce, m), call_expr_argc(ce, n), !call_has_spread(ce), method_min_arity(m, lo), @@ -129,6 +154,7 @@ arity_match(ce, m) :- expr_call_candidate(ce, m), // kept rather than dropped: prune-only means the absence of evidence never removes // the real callee. arity_match(ce, m) :- expr_call_candidate(ce, m), + !sbca_site_rejects(ce, m), !method_arity_raw(m, _, _, _). // ============================================================================ @@ -993,6 +1019,7 @@ expr_resolves_to_method(ce, m) :- arity_match(ce, m), // engine's error, not the program's. call_has_arity_match(ce) :- arity_match(ce, _). expr_resolves_to_method(ce, m) :- expr_call_candidate(ce, m), + !sbca_site_rejects(ce, m), !call_has_arity_match(ce). // ── arity_rejected(CallExpr, Callee, ArgCount, Min, Max) ──────────────────── diff --git a/graph/typescript/engine/framework-behavior/destinations.dl b/graph/typescript/engine/framework-behavior/destinations.dl index 7370082c..35df1c87 100644 --- a/graph/typescript/engine/framework-behavior/destinations.dl +++ b/graph/typescript/engine/framework-behavior/destinations.dl @@ -29,13 +29,18 @@ // literal, a template, a `+` concatenation, a constant (in this module or imported), // and a property of a const object literal (a config module). // +// COVERED: MESSAGING (section 6). A key (a topic, a queue, a message pattern) sent by +// a Nest microservices client (`emit` / `send`) or a kafkajs producer, joined to a +// handler registered on the same key (`@EventPattern` / `@MessagePattern`, a kafkajs +// consumer's `subscribe`), the key read as a route is; one wrapper hop is bound. +// // NOT COVERED YET, stated rather than left to be discovered: // - a router mounted under a prefix (`app.use('/api', router)`): the routes of the // router are compared without it; // - an object-form registration (`fastify.route({ method, url, handler })`); // - a URL built by a wrapper the client calls with the path (the C# rules bind one // hop of that; here the wrapper's own send is reported undetermined); -// - messaging (a topic, a queue) and gRPC. +// - gRPC. // ============================================================================ // remote_edge, remote_unserved, remote_unsent and remote_undetermined are declared in @@ -81,11 +86,17 @@ rd_demand(val) :- rd_prop_value(_, val). rd_demand(c) :- rd_demand(e), expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, _, _, c). +// the object literal a const is initialised with, through `as const` / `satisfies T` +.decl rd_init_obj(c0:symbol, c1:symbol) +rd_init_obj(v, o) :- var_initializer("client", _, o, v), expr_kind("client", "OBJECT_LITERAL", _, o). +rd_init_obj(v, o) :- var_initializer("client", _, i, v), expr_kind("client", k, _, i), expr_kind_is_transparent(k), + expr_child("client", i, _, _, o), expr_kind("client", "OBJECT_LITERAL", _, o). + // `config.reportsUrl`, where `config` is a const initialised with an object literal rd_prop_value(e, val) :- rd_demand(e), expr_kind("client", "PROPERTY_ACCESS", _, e), expr_child("client", e, "RECEIVER", _, r), expr_child("client", e, "PROPERTY_NAME", _, pn), expr_literal_value("client", key, pn), - rd_denotes_var(r, v), var_initializer("client", _, o, v), rd_obj_prop(o, key, val). + rd_denotes_var(r, v), rd_init_obj(v, o), rd_obj_prop(o, key, val). .decl rd_is_str(c0:symbol) .decl rd_is_tmpl(c0:symbol) @@ -411,5 +422,129 @@ remote_undetermined(from, tr, "unresolved_destination") :- ts_remote_transport_h remote_edge(from, to, tr, d, conf) :- remote_edge_via(_, from, to, tr, d, conf). // judged per SEND SITE: a site that linked is not also reported under another spelling remote_unserved(m, tr, d) :- remote_send_at(e, m, tr, d), !remote_edge_via(e, _, _, _, _, _). -// a handler nothing here sends to: its client is in another repository -remote_unsent(m, tr, d) :- remote_serves(m, tr, d), !remote_edge(_, m, tr, d, _). +// a handler nothing here sends to: its client is in another repository. Judged by +// destination alone: a message sent without naming its broker (transport "message") +// is reported under the handler's broker, and still serves it. +.decl remote_reached_at(c0:symbol, c1:symbol) +remote_reached_at(m, d) :- remote_edge(_, m, _, d, _). +remote_unsent(m, tr, d) :- remote_serves(m, tr, d), !remote_reached_at(m, d). + +// ───────────────────────────────────────────────────────────────────────────── +// 6. MESSAGING: a key both ends spell +// ───────────────────────────────────────────────────────────────────────────── +// A producer sends on a KEY (a topic, a queue, a message pattern) and a handler is +// registered on one; neither calls the other. The key is read exactly as a route is +// (section 1): a literal, a const, a const-object member, imported or local. Both +// ends are recognised by what the RECEIVER is declared as, a type imported from the +// messaging package (config-resolution/knobs.dl, ts_msg_client_type), never by the +// method name: `emit` on a Node event emitter is not a broker send. +// +// NOT COVERED YET: a pattern object (`@MessagePattern({ cmd: 'x' })`), a key held in a +// field set at run time (`this.topic`), a receiver whose type is only inferred from a +// factory call in a package outside the graph (`kafka.producer()` with no annotation). + +// the receivers of a call that could be a send or a subscribe +.decl rm_candidate_recv(c0:symbol) +rm_candidate_recv(q) :- call_site("client", "METHOD_CALL", cn, _, q, _, _), ts_msg_send(_, cn, _). +rm_candidate_recv(q) :- call_site("client", "METHOD_CALL", cn, _, q, _, _), ts_msg_subscribe(_, cn, _). +// `this.client`: the field it names, a parameter property included +.decl rm_this_field(c0:symbol, c1:symbol) +rm_this_field(q, f) :- rm_candidate_recv(q), property_access_name(q, n), + expr_child("client", q, "RECEIVER", _, t), expr_type(t, _, tt), field_in_scope(tt, n, "false", f). +// the type reference the receiver is DECLARED with +.decl rm_recv_ref(c0:symbol, c1:symbol) +rm_recv_ref(q, r) :- rm_candidate_recv(q), expr_referenced("client", "PARAMETER", p, q), param_type_ref("client", r, p). +rm_recv_ref(q, r) :- rm_candidate_recv(q), expr_referenced("client", "VARIABLE", v, q), var_type_ref("client", r, v). +rm_recv_ref(q, r) :- rm_this_field(q, f), field_type_ref("client", r, f). +rm_recv_ref(q, r) :- rm_this_field(q, f), param_declares_field("client", f, p), param_type_ref("client", r, p). +// the package a type name is imported from, under the name it exports +// (`import type { ClientProxy as Client } from '@nestjs/microservices'`) +.decl rm_ref_import(c0:symbol, c1:symbol, c2:symbol) +rm_ref_import(r, spec, orig) :- rm_recv_ref(_, r), type_ref("client", _, _, tn, _, _, _, r), type_ref_module("client", m, r), + import_binding("client", _, tn, orig, m, ih), orig != "", rd_import_path(ih, spec). +rm_ref_import(r, spec, tn) :- rm_recv_ref(_, r), type_ref("client", _, _, tn, _, _, _, r), type_ref_module("client", m, r), + import_binding("client", _, tn, "", m, ih), rd_import_path(ih, spec). +.decl rm_recv_role(c0:symbol, c1:symbol) +rm_recv_role(q, role) :- rm_recv_ref(q, r), rm_ref_import(r, spec, name), ts_msg_client_type(spec, name, role). + +// (call, role, transport) +.decl rm_send_call(c0:symbol, c1:symbol, c2:symbol) +rm_send_call(ce, role, tr) :- call_site("client", "METHOD_CALL", cn, _, q, ce, _), ts_msg_send(role, cn, tr), + rm_recv_role(q, role). +.decl rm_sub_call(c0:symbol, c1:symbol, c2:symbol) +rm_sub_call(ce, role, tr) :- call_site("client", "METHOD_CALL", cn, _, q, ce, _), ts_msg_subscribe(role, cn, tr), + rm_recv_role(q, role). + +// the expression holding the key: argument 0, or the `topic` of a record argument, or +// each element of its `topics` +.decl rm_key_arg(c0:symbol, c1:symbol) +rm_key_arg(ce, a) :- rm_send_call(ce, "nest_client", _), expr_child("client", ce, "ARGUMENT", "0", a). +rm_key_arg(ce, v) :- rm_send_call(ce, role, _), role != "nest_client", expr_child("client", ce, "ARGUMENT", "0", o), + rd_obj_prop(o, k, v), ts_msg_record_key(k). +rm_key_arg(ce, v) :- rm_sub_call(ce, _, _), expr_child("client", ce, "ARGUMENT", "0", o), + rd_obj_prop(o, k, v), ts_msg_record_key(k). +rm_key_arg(ce, el) :- rm_sub_call(ce, _, _), expr_child("client", ce, "ARGUMENT", "0", o), + rd_obj_prop(o, k, arr), ts_msg_record_keys(k), expr_kind("client", "ARRAY_LITERAL", _, arr), + expr_child("client", arr, _, _, el). +rd_demand(a) :- rm_key_arg(_, a). +// a key is a value with no computed part (a key expression of either end) +.decl rm_key_expr(c0:symbol) +rm_key_expr(a) :- rm_key_arg(_, a). +rm_key_expr(a) :- rm_handler_key_arg(_, _, a). +.decl rm_key(c0:symbol, c1:symbol) +rm_key(e, k) :- rm_key_expr(e), rd_val(e, k), k != "", !contains("{}", k). + +// ONE WRAPPER HOP: the key is a parameter of the method that sends, and each call of +// that method names it: `place() { return this.send(CMD.place, x); }` over +// `private send(pattern, x) { return this.proxy.send(pattern, x); }`. The send is then +// judged at the CALL of the wrapper, from the method that makes it. +.decl rm_key_param(c0:symbol, c1:symbol, c2:symbol) +rm_key_param(ce, w, pos) :- rm_key_arg(ce, a), expr_referenced("client", "PARAMETER", p, a), + param_decl("client", _, pos, _, w, p), expr_enclosing_method(ce, w). +.decl rm_wrap_arg(c0:symbol, c1:symbol, c2:symbol) +rm_wrap_arg(ce, oc, a) :- rm_key_param(ce, w, pos), call_chain_edge(oc, _, "-", w, "client", _, _), + expr_child("client", oc, "ARGUMENT", pos, a). +rd_demand(a) :- rm_wrap_arg(_, _, a). + +// (site, sending method, transport, key) +.decl rm_send(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +rm_send(ce, from, tr, k) :- rm_send_call(ce, _, tr), rm_key_arg(ce, a), rm_key(a, k), expr_enclosing_method(ce, from). +rm_send(oc, from, tr, k) :- rm_send_call(ce, _, tr), rm_wrap_arg(ce, oc, a), rd_val(a, k), k != "", !contains("{}", k), + expr_enclosing_method(oc, from). + +// ── the handler end ───────────────────────────────────────────────────────── +// a decorated method: `@EventPattern(key, Transport.KAFKA)`; the argument's expression +// is read like any other +.decl rm_handler_key_arg(c0:symbol, c1:symbol, c2:symbol) +rm_handler_key_arg(m, d, a) :- annotation_on("client", n, _, "METHOD_DECLARATION", m, d), ts_msg_handler_decorator(n), + ts_decorator_argument(_, _, _, "0", d, _, _, _, _, _, a, _, _), a != "". +rd_demand(a) :- rm_handler_key_arg(_, _, a). +.decl rm_handler_tr(c0:symbol, c1:symbol) +rm_handler_tr(d, tr) :- rm_handler_key_arg(_, d, _), annotation_arg("client", _, v, _, "1", _, d, _), + ts_msg_transport_member(v, tr). +.decl rm_handler_has_tr(c0:symbol) +rm_handler_has_tr(d) :- rm_handler_tr(d, _). +// (handler, transport, key) +.decl rm_serve(c0:symbol, c1:symbol, c2:symbol) +rm_serve(m, tr, k) :- rm_handler_key_arg(m, d, a), rm_key(a, k), rm_handler_tr(d, tr). +rm_serve(m, tr, k) :- rm_handler_key_arg(m, d, a), rm_key(a, k), !rm_handler_has_tr(d), ts_remote_transport_message(tr). +// a subscription: the function that subscribes is where the messages arrive +rm_serve(m, tr, k) :- rm_sub_call(ce, _, tr), rm_key_arg(ce, a), rm_key(a, k), expr_enclosing_method(ce, m). + +// ── the join ──────────────────────────────────────────────────────────────── +// Same key; the transports agree, or one end does not say. The edge is reported under +// the end that names its broker. +.decl rm_edge_tr(c0:symbol, c1:symbol, c2:symbol) +rm_edge_tr(a, a, a) :- rm_send(_, _, a, _). +rm_edge_tr(a, b, b) :- rm_send(_, _, a, _), rm_serve(_, b, _), ts_remote_transport_message(a), a != b. +rm_edge_tr(a, b, a) :- rm_send(_, _, a, _), rm_serve(_, b, _), ts_remote_transport_message(b), a != b. +remote_edge_via(e, from, to, tr, k, conf) :- rm_send(e, from, st, k), rm_serve(to, ht, k), rm_edge_tr(st, ht, tr), + from != to, ts_remote_confidence_exact(conf). +remote_serves(m, tr, k) :- rm_serve(m, tr, k). +remote_send_at(e, from, tr, k) :- rm_send(e, from, tr, k). +// a send whose key cannot be read, and that no caller names either +.decl rm_send_keyed(c0:symbol) +rm_send_keyed(ce) :- rm_send(ce, _, _, _). +rm_send_keyed(ce) :- rm_wrap_arg(ce, oc, _), rm_send(oc, _, _, _). +remote_undetermined(from, tr, "unresolved_destination") :- rm_send_call(ce, _, tr), !rm_send_keyed(ce), + expr_enclosing_method(ce, from). diff --git a/graph/typescript/engine/projections/modules.dl b/graph/typescript/engine/projections/modules.dl index f3c1e237..dc2a1429 100644 --- a/graph/typescript/engine/projections/modules.dl +++ b/graph/typescript/engine/projections/modules.dl @@ -165,3 +165,14 @@ sbca_value(v) :- ts_module(_, _, _, _, _, _, _, _, _, _, _, _, _, _, tsconfig, _ sbca_regime("STRICT") :- sbca_value("true"), !sbca_value("false"). sbca_regime("LOOSE") :- sbca_value("false"), !sbca_value("true"). sbca_regime("MIXED") :- sbca_value("true"), sbca_value("false"). + +// ── module_sbca_governed(ModuleHash, Flag) — the flag a CALL SITE is compiled under ── +// MIXED above is the right answer for the project and the wrong one for a call: each +// file is compiled under the tsconfig that governs it, so a call in a strict program +// answers with CallableFunction and a call in a loose one with Function, in the same +// repository. overload.dl reads this per call site to settle what MIXED hedged. +// The same vote guard as sbca_value: a module no tsconfig claims carries the resolver's +// default, not a measurement, and keeps the project's hedge. +module_sbca_governed(h, v) :- ts_module(_, _, _, _, _, _, _, _, _, _, _, _, _, _, tsconfig, _, _, _, _, _, _, _, _, _, _, _, h, v), + v != "", + tsconfig != "". 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/structural-satisfaction.dl b/graph/typescript/engine/resolution/structural-satisfaction.dl index d911f708..c2ab16df 100644 --- a/graph/typescript/engine/resolution/structural-satisfaction.dl +++ b/graph/typescript/engine/resolution/structural-satisfaction.dl @@ -17,10 +17,14 @@ // // Keeping it off the primary path is deliberate. It is the one relation in this // engine that could FABRICATE rather than over-approximate, because it is name-based -// and does not compare member TYPES; a class with a `read()` that takes different -// arguments would still be counted as satisfying `Readable`. That is acceptable for -// widening a prune-only test and for a labelled reachability fan; it would not be -// acceptable as the answer to "what does this call resolve to". +// and does not compare member TYPES; a class with a `read(s: string)` would still be +// counted as satisfying `Readable { read(n: number) }`. Two cheap tests keep the worst +// of it out: a method that needs MORE arguments than the target passes is rejected +// (sat_arity_conflict), and a class no code can ever hand to the interface's slot is +// not an implementor (sat_can_meet). That is acceptable for widening a prune-only test +// and for a labelled reachability fan; it would not be acceptable as the answer to +// "what does this call resolve to", and a consumer never treats the `structural` basis +// as a declared contract. // // ── THE SOUNDNESS ARGUMENT, STATED ────────────────────────────────────────── // Direction: SOURCE is assignable to TARGET when the source has a member for every @@ -98,13 +102,68 @@ sat_covered(s, t, name) :- sat_pair_seed(s, t), sat_cover_count(s, t, n) :- sat_pair_seed(s, t), n = count : { sat_covered(s, t, _) }. +// ── sat_arity_conflict(Source, Target) — a shared name that cannot be called the same way +// A NAME MATCH IS NOT A MEMBER MATCH. `publish(name, payload)` shares a name with the +// interface's `publish(envelope)` and is not assignable to it: a method that needs more +// arguments than the target's signature passes is rejected by the compiler, whatever +// the types. So a covered METHOD name conflicts when no source method of that name +// needs at most as many arguments as some target method of that name takes. Fewer +// required parameters is fine (`read()` satisfies `read(n: number)`), and so is an +// optional or rest parameter. Only a pair where BOTH sides have arity facts can conflict: +// a member without them (a field holding a function) is left to the name test. +sat_target_method(t, name, m) :- target_required_member(t, name), + scope_sibling(t, sib), + declared_method(sib, name, "false", m). +sat_target_method(t, name, m) :- target_required_member(t, name), + group_of(t, g), + ancestor_of_merged_group(_, g, anc), + scope_sibling(anc, asib), + declared_method(asib, name, "false", m). +sat_source_method(s, name, m) :- sat_pair_seed(s, _), + scope_sibling(s, sib), + declared_method(sib, name, "false", m). +sat_source_method(s, name, m) :- sat_pair_seed(s, _), + group_of(s, g), + ancestor_of_merged_group(_, g, anc), + scope_sibling(anc, asib), + declared_method(asib, name, "false", m). +sat_arity_ok(s, t, name) :- sat_covered(s, t, name), + sat_source_method(s, name, sm), + sat_target_method(t, name, tm), + method_min_arity(sm, lo), + method_max_arity(tm, hi), + lo <= hi. +sat_arity_conflict(s, t) :- sat_covered(s, t, name), + sat_source_method(s, name, sm), + method_min_arity(sm, _), + sat_target_method(t, name, tm), + method_max_arity(tm, _), + !sat_arity_ok(s, t, name). +// A `private` or `protected` method never satisfies an interface member of its name: the +// compiler rejects the assignment ("property is private in type S but not in type T"), +// so a class whose only method of that name is hidden is not a conformer. A covered name +// the class also declares visibly (or as a field) is left alone. +sat_member_hidden("PRIVATE_ACCESS"). +sat_member_hidden("PROTECTED_ACCESS"). +sat_member_hidden("PRIVATE_NAME_ACCESS"). +sat_visible_source_method(s, name) :- sat_source_method(s, name, m), + method_access(_, acc, m), + !sat_member_hidden(acc). +sat_arity_conflict(s, t) :- sat_covered(s, t, name), + sat_source_method(s, name, m), + method_access(_, acc, m), + sat_member_hidden(acc), + !sat_visible_source_method(s, name). + // ── type_satisfies(SourceTypeHash, TargetTypeHash) ────────────────────────── -// Every required member covered. `k > 0` excludes the empty interface, which +// Every required member covered, and no covered method that could not be called with +// the target's arguments. `k > 0` excludes the empty interface, which // everything satisfies and which therefore carries no information — admitting it // would make every class an implementor of every marker interface in the tree. type_satisfies(s, t) :- sat_cover_count(s, t, k), target_required_count(t, k), - k > 0. + k > 0, + !sat_arity_conflict(s, t). // A nominal implements clause is satisfaction too, and it is the authoritative kind: // the programmer asserted it and the compiler checked it. Included here so consumers @@ -120,7 +179,42 @@ target_has_nominal_implementor(t) :- implementors(t, _). structural_implementor(t, s) :- type_satisfies(s, t), satisfaction_target(t), s != t, - !target_has_nominal_implementor(t). + !target_has_nominal_implementor(t), + !sat_cross_program(s, t), + sat_can_meet(s, t). + +// ── sat_cross_program(Source, Target) — a shape match no value can cross (#1574) ── +// A CLIENT GLOBAL belongs to one program (module-graph.dl). A class in ANOTHER program +// that happens to have the global's members — production's `Pipeline.map` against a test +// fixture's hand-written `interface Array { map }` — was counted as its implementor, so +// every fixture `xs.map(cb)` dispatched into production code and production's own +// `this.items.map(f)` dispatched into the fixture's classes: callback reach then spanned +// every program in the repository. A value of the class can reach a receiver typed by +// that global only in code that sees both, so the pair is dropped when NEITHER program +// can see the other's file: the class's module is outside the global's program, and the +// global's file is outside the class's. Governed-by or under-the-directory-of, the same +// test program_sees makes, so a root `types/global.d.ts` keeps every nested package's +// conformers, and a nested package's global keeps a root class its code may import. +// Only pairs of two different governing tsconfigs are asked, so this is a handful of +// prefix tests, not (modules) x (programs). A module with no tsconfig is never cut. +path_key(p) :- module_tsconfig(_, p). +tsconfig_dir(p, d) :- module_tsconfig(_, p), path_dir(p, d). +sat_cross_program_pair(s, t, ms, pt, mt, ps) :- sat_pair_seed(s, t), + client_global_program(t, pt), + type_module("client", ms, s), module_tsconfig(ms, ps), ps != pt, + type_module("client", mt, t). +sat_tree_ask(ms, pt) :- sat_cross_program_pair(_, _, ms, pt, _, _). +sat_tree_ask(mt, ps) :- sat_cross_program_pair(_, _, _, _, mt, ps). +module_in_program_tree(m, p) :- sat_tree_ask(m, p), module_tsconfig(m, p). +module_in_program_tree(m, p) :- sat_tree_ask(m, p), + tsconfig_dir(p, d), + module_file("client", fp, m), + n = strlen(d), + strlen(fp) >= n, + substr(fp, 0, n) = d. +sat_cross_program(s, t) :- sat_cross_program_pair(s, t, ms, pt, mt, ps), + !module_in_program_tree(ms, pt), + !module_in_program_tree(mt, ps). // ── …AND A CONFORMER THE PROGRAM ACTUALLY CONSTRUCTS, EVEN THEN (#428) ───── // The gate above is right about EVIDENCE and wrong about what it does with it. Where a @@ -169,7 +263,67 @@ structural_implementor(t, s) :- type_satisfies(s, t), s != t, target_has_nominal_implementor(t), !implementors(t, s), - type_instantiated(s, _). + type_instantiated(s, _), + !sat_cross_program(s, t), + sat_can_meet(s, t). + +// ── sat_can_meet(Source, Target) — can a value of the class ever reach the interface's slot +// A SHAPE MATCH BETWEEN TWO CODEBASES THAT NEVER MEET IS NOT A CONFORMANCE. In a monorepo +// one service's event bus and another service's publisher interface share a method name, +// and nothing in either program can hand the one to the other. For an instance of S to +// flow into a slot typed T, some code has to see both: S's own module, or a module that +// uses S as a value (`new S()`, `useClass: S`), must import T's module, directly or +// through other modules and re-exports. Admitted without the walk: +// * S and T in one tsconfig program, or in one module — the program sees both; +// * T a library interface, or either side in a global script — no import is needed +// to see it, so the import graph proves nothing. +// A program-level dependency is deliberately NOT enough: a test that boots two services +// together makes each app depend on the other, and every name the two share would come +// back. The import walk runs only for the pairs left, from the modules that hold S. +sat_meet_pair(s, t) :- type_satisfies(s, t), + satisfaction_target(t), + s != t, + !implementors(t, s). +sat_client_type(t) :- type_module("client", _, t). +sat_meets_trivially(s, t) :- sat_meet_pair(s, t), type_program(s, p), type_program(t, p). +sat_meets_trivially(s, t) :- sat_meet_pair(s, t), type_module("client", m, s), type_module("client", m, t). +sat_meets_trivially(s, t) :- sat_meet_pair(s, t), !sat_client_type(t). +sat_meets_trivially(s, t) :- sat_meet_pair(s, t), type_module("client", m, s), module_is_global("client", m). +sat_meets_trivially(s, t) :- sat_meet_pair(s, t), type_module("client", m, t), module_is_global("client", m). +sat_meet_open(s, t) :- sat_meet_pair(s, t), !sat_meets_trivially(s, t). + +sat_holder_module(s, m) :- sat_meet_open(s, _), type_module("client", m, s). +sat_holder_module(s, m) :- sat_meet_open(s, _), + expr_referenced("client", "TYPE", s, e), + expr_module("client", m, e). +sat_module_dep(a, b) :- import_binding("client", _, _, _, a, h), + import_target_module(h, "client", b). +sat_module_dep(a, b) :- export_decl("client", _, _, _, a, h), + export_target_module(h, "client", b). +sat_module_reach(m, m) :- sat_holder_module(_, m). +sat_module_reach(o, b) :- sat_module_reach(o, a), + sat_module_dep(a, b). + +sat_can_meet(s, t) :- sat_meets_trivially(s, t). +sat_can_meet(s, t) :- sat_meet_open(s, t), + sat_holder_module(s, o), + type_module("client", mt, t), + sat_module_reach(o, mt). +// THE WALK STOPS WHERE AN IMPORT LEADS OUT OF THE CLIENT: a workspace package whose +// manifest points at build output that is not there, or at a compiled .d.ts, binds no +// client module, so the walk cannot tell whether T's module is behind it. An import +// there that NAMES the interface is the evidence instead: the class's module, or one it +// reaches, brings in a type called T from a place the walk cannot follow. +sat_opaque_import_name(m, name) :- import_binding("client", _, name, _, m, h), + !import_target_module(h, "client", _). +sat_opaque_import_name(m, name) :- import_binding("client", _, _, name, m, h), + name != "", + !import_target_module(h, "client", _). +sat_can_meet(s, t) :- sat_meet_open(s, t), + sat_holder_module(s, o), + sat_module_reach(o, m), + sat_opaque_import_name(m, name), + type_decl("client", name, _, _, _, _, t). // ── satisfaction_unmeasured(TargetTypeHash, Name) ─────────────────────────── // EVERY SUPPRESSION COUNTABLE. A required member of a tested interface that NO 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 b8c6aba6..acc74483 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), @@ -212,30 +208,183 @@ holder_value(p, a) :- expr_child("client", ce, "ARGUMENT", pos, a), // ============================================================================ call_runs_client_body(ce) :- call_runs_method(ce, m), method_prov(m, "client"). hof_boundary_site(ce) :- call_site("client", _, _, _, _, ce, _), !call_runs_client_body(ce). +// A Map or Set stores, finds and drops its argument and never calls it (`subs.add(h)`, +// `handlers.set(k, h)`, `seen.has(h)`); only `forEach` runs what it is handed. The function +// is reached where it is taken back out and called, not where it is stored. +collection_store_site(ce) :- expr_resolves_to_method(ce, m), method_decl("lib", mn, _, _, m), mn != "forEach", + method_owner("lib", t, m), type_decl("lib", tn, _, _, _, _, t), keyed_collection_name(tn). +collection_store_site(ce) :- call_site("client", _, mn, _, recv, ce, _), recv != "", mn != "forEach", + expr_type(recv, _, t), type_decl(_, tn, _, _, _, _, t), keyed_collection_name(tn). +collection_store_site(ce) :- call_site("client", _, mn, _, recv, ce, _), recv != "", mn != "forEach", + collection_receiver(recv). +// Without the standard library staged there is no Map declaration to resolve to, so the +// receiver is recognised by the reference it was declared with, or by the `new Map()` an +// un-annotated variable is initialised with. Recognised by NAME only where the program +// does not declare that name itself (builtin_container_ref). +collection_receiver(e) :- expr_decl_ref(e, r), keyed_collection_ref(r). +collection_receiver(e) :- new_expression_ref(e, r), keyed_collection_ref(r). +collection_receiver(e) :- expr_referenced("client", "VARIABLE", v, e), !var_has_declared_type(v), + var_initializer("client", _, init, v), init != "", collection_receiver(init). +keyed_collection_ref(r) :- type_ref(_, "TYPE_REFERENCE", _, tn, _, _, _, r), keyed_collection_name(tn), + builtin_container_ref(r). +keyed_collection_name("Map"). +keyed_collection_name("ReadonlyMap"). +keyed_collection_name("WeakMap"). +keyed_collection_name("Set"). +keyed_collection_name("ReadonlySet"). +keyed_collection_name("WeakSet"). value_branch(a, a) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), !expr_kind("client", "CALL_EXPRESSION", _, x), # `xs.map(make())` hands over what make RETURNS expr_callable(x, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), expr_referenced("client", k, h, x), holder_ref_kind(k), holder_holds_function(h, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), ts_field_access_target(x, f), holder_holds_function(f, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), expr_kind("client", "CALL_EXPRESSION", _, x), call_runs(x, f), holder_holds_function(f, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), 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/python-gates/function-exports.ts b/parser/src/test/python-gates/function-exports.ts new file mode 100644 index 00000000..899464a8 --- /dev/null +++ b/parser/src/test/python-gates/function-exports.ts @@ -0,0 +1,96 @@ +/** + * A module-level `async def`, generator or async generator is a module export (#1525). + * + * `from jobs import sync_orders` is resolved against a per-module export index. That + * index admitted a module-level function only when its method kind was FUNCTION, and + * the kind of a `def` names what a call RETURNS: `async def` is ASYNC_FUNCTION, one + * with `yield` is GENERATOR or ASYNC_GENERATOR. So the import of any of those got no + * entity, and a value use of the name in the importing module (a callback, a job list) + * was an IMPORT reference that impact never counted, while the same use of a plain + * `def` was a METHOD reference. + * + * Controls: a plain `def` resolves as before, and a nested `async def` is still not an + * export (it has an enclosing member, so importing it resolves to no function). + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { PythonProjectAnalyzer } from '@/workflows/python/python-project-analyzer'; + +const JOBS = [ + 'async def sync_orders(): return 1', + 'def stream_orders(): yield 1', + 'async def stream_users(): yield 1', + 'def count_orders(): return 1', + 'def outer():', + ' async def inner(): return 1', + ' return inner', + '', +].join('\n'); + +const REGISTRY = [ + 'from jobs import count_orders, inner, stream_orders, stream_users, sync_orders', + '', + 'JOBS = [sync_orders, stream_orders, stream_users, count_orders, inner]', + '', +].join('\n'); + +/** imported name -> [resolves to the module function of that name, what it proves] */ +const EXPECTED: ReadonlyArray = [ + ['sync_orders', true, 'an async def'], + ['stream_orders', true, 'a generator'], + ['stream_users', true, 'an async generator'], + ['count_orders', true, 'control: a plain def'], + ['inner', false, 'control: a nested async def is not a module export'], +]; + +export async function functionExports(): Promise { + const problems: string[] = []; + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'py-fnexports-')); + const source = path.join(root, 'src'); + fs.mkdirSync(source, { recursive: true }); + fs.writeFileSync(path.join(source, 'jobs.py'), JOBS); + fs.writeFileSync(path.join(source, 'registry.py'), REGISTRY); + + const outputDir = path.join(root, 'out'); + await new PythonProjectAnalyzer().analyze({ + rootDir: source, + outputDir, + baseMservPath: '/repo', + serviceVersionLinkHash: 'SERVICE_VERSION_' + '0'.repeat(32), + }); + + const tsv = (file: string): Record[] => { + const lines = fs.readFileSync(path.join(outputDir, file), 'utf-8').split('\n').filter(Boolean); + const header = lines[0]!.split('\t'); + return lines.slice(1).map((l) => { + const cells = l.split('\t'); + return Object.fromEntries(header.map((h, i) => [h, cells[i] ?? ''])); + }); + }; + const methods = new Map(tsv('all-python-methods.csv').map((m) => [m.pyMethodUniqueHash, m.name])); + const imports = tsv('all-python-imports.csv'); + let checked = 0; + for (const [name, exported, what] of EXPECTED) { + const row = imports.find((i) => i.originalName === name || i.simpleName === name); + if (!row) { + problems.push(`${name}: no import row`); + continue; + } + checked++; + const isFunction = row.resolvedTargetKind === 'FUNCTION' && methods.get(row.resolvedTargetHash) === name; + if (isFunction !== exported) { + problems.push( + `${name} (${what}): resolved to ${row.resolvedTargetKind} ${row.resolvedTargetHash}, ` + + `expected ${exported ? 'the FUNCTION of that name' : 'no module function'}` + ); + } + } + if (checked < EXPECTED.length) problems.push(`only ${checked} of ${EXPECTED.length} imports checked`); + + fs.rmSync(root, { recursive: true, force: true }); + console.log(` ${checked} imported names checked`); + for (const problem of problems) console.log(` FAIL ${problem}`); + return problems.length === 0 ? 0 : 1; +} diff --git a/parser/src/test/python-tests.ts b/parser/src/test/python-tests.ts index feaf252a..5215d0dc 100644 --- a/parser/src/test/python-tests.ts +++ b/parser/src/test/python-tests.ts @@ -38,6 +38,7 @@ import { splatCallee } from './python-gates/splat-callee'; import { cachedProperty } from './python-gates/cached-property'; import { fieldLines } from './python-gates/field-lines'; import { pep604Union } from './python-gates/pep604-union'; +import { functionExports } from './python-gates/function-exports'; const VERIFIED = 'src/test-data/python/verified'; const GOLDEN = path.join(VERIFIED, '_golden'); @@ -596,6 +597,8 @@ const CHECKS: Check[] = [ proves: 'async for and async with are distinguishable from their sync forms, so the right protocol edge can be chosen' }, { name: 'soft-keyword type call', run: softKeywordTypeCall, proves: 'type(obj).attr = v is an assignment through a call, and the subscript form is not' }, + { name: 'function exports', run: functionExports, + proves: 'a module-level async def, generator or async generator is importable by name, as a plain def is' }, { name: 'construct x position', run: constructPositions, proves: 'constructs hold in EVERY syntactic position, not just the one the corpus uses' }, ]; 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