diff --git a/bin/axiomcode b/bin/axiomcode index 649bdad65..e01ccc31b 100755 --- a/bin/axiomcode +++ b/bin/axiomcode @@ -17,8 +17,8 @@ # (run.source_version). Defaults to the source's git commit, or # v1.0.0 outside a checkout. # --exclude-tests Exclude test code. Included by default. -# --language Restrict the run to one language: java, typescript, python or -# javascript. +# --language [,…] Restrict the run to these languages: java, typescript, python, +# javascript or csharp. # --debug Also write csv/*.csv and keep raw/ beside each graph. # # Engine options: @@ -57,7 +57,13 @@ if [ -L "$self" ]; then fi self="$r" fi -ROOT="$(d="$(cd "$(dirname "$self")" && pwd)"; while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="$(dirname "$d")"; done; echo "$d")" +# NO PROGRAM IS STARTED TO FIND IT: the parent of a path is taken by parameter expansion, not `dirname`. Every query goes +# through here, and under Git Bash on Windows each program started is a fork emulation that costs tens of milliseconds +# idle and far more on a busy machine; the walk used to start one per level, the verb check below four more. +case "$self" in */*) d="${self%/*}";; *) d=.;; esac +d="$(cd "${d:-/}" && pwd)" +while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="${d%/*}"; d="${d:-/}"; done +ROOT="$d" # No marker found means every path built from ROOT is nonsense; say so here rather than fail # later on a path the user cannot interpret. [ -d "$ROOT/graph" ] || { echo "axiomcode: cannot locate the package root from $0 (resolved to $self)" >&2; exit 2; } @@ -76,8 +82,20 @@ 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. -query_verbs(){ [ -f "$QUERY" ] && bash "$QUERY" --verbs 2>/dev/null | grep -vx tests || true; } -is_query_verb(){ query_verbs | grep -qx -- "$1"; } +# 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. +QVERBS=() +load_verbs(){ + [ ${#QVERBS[@]} -gt 0 ] && return 0; [ -f "$QUERY" ] || return 0 + local l v x re='^ ([a-z|-]*)\) *exec ' + 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 + 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 @@ -177,7 +195,11 @@ case "$cmd" in [ -n "$src" ] || src="${pos[0]:-}"; [ -n "$out" ] || out="${pos[1]:-}" [ -n "$src" ] && [ -n "$out" ] || die "usage: axiomcode [--library [,…]] [options] (try --help)" [ -d "$src" ] || die "not a directory: $src" - [ -z "$lang" ] || [ -d "$ROOT/graph/$lang" ] || die "no engine for --language=$lang (have: $(ls -d "$ROOT"/graph/*/engine 2>/dev/null | sed 's#.*/graph/##; s#/engine##' | tr '\n' ' '))" + # --language takes one language or a comma list of them (a repository written in several) + IFS=',' read -ra want <<< "$lang" + for l in ${want[@]+"${want[@]}"}; do + [ -d "$ROOT/graph/$l" ] || die "no engine for --language=$l (have: $(ls -d "$ROOT"/graph/*/engine 2>/dev/null | sed 's#.*/graph/##; s#/engine##' | tr '\n' ' '))" + done [ -n "$version" ] || version="$(git -C "$src" rev-parse HEAD 2>/dev/null || echo v1.0.0)" popts+=(--version "$version") mkdir -p "$out"; out="$(cd "$out" && pwd)"; int="$out/.intermediate"; ir="$int/ir" @@ -233,8 +255,16 @@ case "$cmd" in 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; } langs=("${found[@]}") - if [ -n "$lang" ]; then + if [ ${#want[@]} -eq 1 ]; then case " ${found[*]} " in *" $lang "*) langs=("$lang");; *) echo "❌ --language=$lang, but the IR holds only: ${found[*]}" >&2; exit 1;; esac + elif [ ${#want[@]} -gt 1 ]; then + # A LANGUAGE ASKED FOR AND NOT FOUND IS NAMED, NEVER DROPPED IN SILENCE. The caller counted files by + # extension; the parser finds PROJECTS, and a file no project claims is not in the IR. The others are + # still solved, and the caller is told which one is missing. + langs=(); missing=() + for l in "${want[@]}"; do case " ${found[*]} " in *" $l "*) langs+=("$l");; *) missing+=("$l");; esac; done + [ ${#missing[@]} -gt 0 ] && echo "⚠️ asked for ${missing[*]}, but the IR holds no such language (it holds: ${found[*]})" >&2 + [ ${#langs[@]} -gt 0 ] || { echo "❌ --language=$lang, but the IR holds only: ${found[*]}" >&2; exit 1; } fi echo "▶ languages: ${langs[*]}" # ONE LANGUAGE'S FAILURE MUST NOT COST THE OTHERS THEIR GRAPHS. Under `set -e` this loop diff --git a/gemini-extension.json b/gemini-extension.json index defcedc65..5d36ac9bd 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.2", + "version": "0.1.3", "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 8b9b8c290..01bb715a7 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -491,7 +491,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `DYNAMIC_IMPORT_CALL` | typescript | `import(…)`. | | `DECORATOR_CALL` | typescript | A decorator application `@d` / `@d(…)`. | | `OPTIONAL_CALL` | typescript | `f?.(…)`. | -| `JSX_COMPONENT_CALL` | typescript | `` (reserved by the parser; emitted by nothing yet). | +| `JSX_COMPONENT_CALL` | typescript | `` — a JSX element naming a component; `
` emits no site. | | `PROPERTY_READ` | typescript, javascript | Reading `obj.x` where `x` is a `get` accessor runs the getter (engine-authored). No written call; the site is the property-access expression. A compound assignment or `++` reads before it writes, so it carries this and PROPERTY_WRITE. | | `PROPERTY_WRITE` | typescript, javascript | Assigning `obj.x = v` where `x` is a `set` accessor runs the setter (engine-authored). No written call; the site is the property-access expression on the left. | | `FUNCTION_CALL` | javascript | `f(…)` — a bare callee, resolved by the binder. | @@ -507,6 +507,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `TAGGED_TEMPLATE_CALL` | javascript | tag`…`. | | `DYNAMIC_CODE_CALL` | javascript | `eval(…)` / `new Function(…)` — unknowable by construction. | | `DYNAMIC_IMPORT_CALL` | javascript | `import(…)` — a module load that is also a site. | +| `JSX_ELEMENT` | javascript | `` — the renderer runs the component (a function component, or a class component's constructor and `render`) with the element's attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site. | | `SIMPLE_CALL` | python | `f(…)` — a bare name. | | `METHOD_CALL` | python | `obj.m(…)`. | | `CHAINED_CALL` | python | `a.b().c(…)` — the receiver is itself a call. | @@ -598,7 +599,7 @@ THE DISPATCH ENVELOPE: (base method, method that may run instead) for every call | value | languages | meaning | |---|---|---| -| `nominal` | java, typescript | A written extends/implements reaches the candidate's owner from the base's owner. The strongest evidence there is: the author declared the relationship. | +| `nominal` | java, typescript, csharp | A written extends/implements reaches the candidate's owner from the base's owner. The strongest evidence there is: the author declared the relationship. | | `structural` | typescript | No declaration; the candidate's owner satisfies the base's owner by SHAPE. Emitted only for supertypes with no nominal implementor at all, so it never competes with a declared answer — but it is a heuristic, and a consumer that wants declarations only filters it out. | | `value` | java, typescript, csharp, python | A function stored in a field, variable or parameter whose type is the base's callable type; a call through that holder may run it. In TypeScript also a function written as an implementation of an interface's member signature: an object-literal member of a literal typed by the interface (#1208), or a function assigned to the member through a property chain, `inst.i.run = (x) => …` (#1283). In C# also what one delegate member is assigned from another (`a.Run = a.Parse`). In Python, a function assigned onto an instance's member from outside its class, which shadows the class's method of that name. The base is the signature the call resolves to (bodiless), so the pair is how a walk over callers reaches the function that runs. Flow-derived rather than declared: a consumer that wants declarations only filters it out. | | `mro` | python | The subtype's C3 linearisation picks the candidate for that attribute name. Not merely "the subtype declares this name" — a name a sibling base wins is attributed to that sibling. | @@ -609,7 +610,7 @@ THE DISPATCH ENVELOPE: (base method, method that may run instead) for every call ### `overrides` -Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java only, and kept for compatibility — it is exactly `dispatch_candidates` filtered to `basis = nominal`. Prefer `dispatch_candidates`, which is populated in every language. +Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java and C# only, and kept for compatibility — it is exactly `dispatch_candidates` filtered to `basis = nominal`. Prefer `dispatch_candidates`, which is populated in every language. | # | column | type | key | null | idx | meaning | |---|---|---|---|---|---|---| @@ -620,7 +621,6 @@ Virtual-dispatch pairs: (base method, overriding method) wherever a call to the - **typescript** — EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`. - **python** — 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. -- **csharp** — EMPTY — C# records the dispatch envelope in dispatch_candidates, not as override rows. ### `entry_points` diff --git a/graph/bundle/languages.ts b/graph/bundle/languages.ts index 0aad38268..cfb657d2b 100644 --- a/graph/bundle/languages.ts +++ b/graph/bundle/languages.ts @@ -353,7 +353,10 @@ const CSHARP: LanguageAdapter = { prefixes: { method: 'CS_METHOD_', type: 'CS_TYPE_', expression: 'CS_EXPRESSION_', module: 'CS_MODULE_' }, raw: { callEdges: CALL_EDGES, - typeAncestors: { file: 'resolution-type-ancestor.csv', columns: [1, 2] }, + // keyed on the declaration, not the partial-type group type_ancestor itself uses, + // so it joins types.id + typeAncestors: { file: 'resolution-type-ancestor-type.csv', columns: [1, 2] }, + overrides: { file: 'resolution-virtual-override.csv', columns: [0, 1] }, entryPoints: { file: 'entry-point.csv', columns: [0, 1] }, entryReachable: { file: 'entry-reachable.csv', columns: [0] }, dispatchCandidates: { file: 'dispatch-candidates.csv', columns: [0, 1, 2] }, diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index b2aaec37b..9f0229d9f 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -143,7 +143,7 @@ export const CORE_TABLES: readonly TableSpec[] = [ }, { name: 'overrides', - description: 'Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java only, and kept for compatibility — it is exactly `dispatch_candidates` filtered to `basis = nominal`. Prefer `dispatch_candidates`, which is populated in every language.', + description: 'Virtual-dispatch pairs: (base method, overriding method) wherever a call to the base may run the override. Java and C# only, and kept for compatibility — it is exactly `dispatch_candidates` filtered to `basis = nominal`. Prefer `dispatch_candidates`, which is populated in every language.', columns: [ { name: 'method_id', type: 'TEXT', key: true, indexed: true, description: 'FK → methods.id — the base (declared) method.' }, { name: 'overriding_method_id', type: 'TEXT', key: true, indexed: true, description: 'FK → methods.id — the override in a subtype.' }, @@ -623,7 +623,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'DYNAMIC_IMPORT_CALL', languages: T, meaning: '`import(…)`.' }, { table: 'call_edges', column: 'kind', value: 'DECORATOR_CALL', languages: T, meaning: 'A decorator application `@d` / `@d(…)`.' }, { table: 'call_edges', column: 'kind', value: 'OPTIONAL_CALL', languages: T, meaning: '`f?.(…)`.' }, - { table: 'call_edges', column: 'kind', value: 'JSX_COMPONENT_CALL', languages: T, meaning: '`` (reserved by the parser; emitted by nothing yet).' }, + { table: 'call_edges', column: 'kind', value: 'JSX_COMPONENT_CALL', languages: T, meaning: '`` — a JSX element naming a component; `
` emits no site.' }, { table: 'call_edges', column: 'kind', value: 'PROPERTY_READ', languages: ['typescript', 'javascript'], meaning: 'Reading `obj.x` where `x` is a `get` accessor runs the getter (engine-authored). No written call; the site is the property-access expression. A compound assignment or `++` reads before it writes, so it carries this and PROPERTY_WRITE.' }, { table: 'call_edges', column: 'kind', value: 'PROPERTY_WRITE', languages: ['typescript', 'javascript'], meaning: 'Assigning `obj.x = v` where `x` is a `set` accessor runs the setter (engine-authored). No written call; the site is the property-access expression on the left.' }, // — JavaScript (the parser's JsCallKind) @@ -640,6 +640,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'TAGGED_TEMPLATE_CALL', languages: S, meaning: 'tag`…`.' }, { table: 'call_edges', column: 'kind', value: 'DYNAMIC_CODE_CALL', languages: S, meaning: '`eval(…)` / `new Function(…)` — unknowable by construction.' }, { table: 'call_edges', column: 'kind', value: 'DYNAMIC_IMPORT_CALL', languages: S, meaning: '`import(…)` — a module load that is also a site.' }, + { table: 'call_edges', column: 'kind', value: 'JSX_ELEMENT', languages: S, meaning: '`` — the renderer runs the component (a function component, or a class component\'s constructor and `render`) with the element\'s attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site.' }, // — Python (the parser's callKind, plus engine-authored decorator/metaclass forms) { table: 'call_edges', column: 'kind', value: 'SIMPLE_CALL', languages: P, meaning: '`f(…)` — a bare name.' }, { table: 'call_edges', column: 'kind', value: 'METHOD_CALL', languages: P, meaning: '`obj.m(…)`.' }, @@ -691,7 +692,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'entry_points', column: 'reason', value: 'orm_hook', languages: P, meaning: 'A lifecycle or validation hook registered by decoration. The data layer calls it; nothing in the client does.' }, // dispatch_candidates.basis - { table: 'dispatch_candidates', column: 'basis', value: 'nominal', languages: ['java', 'typescript'], meaning: 'A written extends/implements reaches the candidate\'s owner from the base\'s owner. The strongest evidence there is: the author declared the relationship.' }, + { table: 'dispatch_candidates', column: 'basis', value: 'nominal', languages: ['java', 'typescript', 'csharp'], meaning: 'A written extends/implements reaches the candidate\'s owner from the base\'s owner. The strongest evidence there is: the author declared the relationship.' }, { table: 'dispatch_candidates', column: 'basis', value: 'structural', languages: T, meaning: 'No declaration; the candidate\'s owner satisfies the base\'s owner by SHAPE. Emitted only for supertypes with no nominal implementor at all, so it never competes with a declared answer — but it is a heuristic, and a consumer that wants declarations only filters it out.' }, { table: 'dispatch_candidates', column: 'basis', value: 'value', languages: ['java', 'typescript', 'csharp', 'python'], meaning: 'A function stored in a field, variable or parameter whose type is the base\'s callable type; a call through that holder may run it. In TypeScript also a function written as an implementation of an interface\'s member signature: an object-literal member of a literal typed by the interface (#1208), or a function assigned to the member through a property chain, `inst.i.run = (x) => …` (#1283). In C# also what one delegate member is assigned from another (`a.Run = a.Parse`). In Python, a function assigned onto an instance\'s member from outside its class, which shadows the class\'s method of that name. The base is the signature the call resolves to (bodiless), so the pair is how a walk over callers reaches the function that runs. Flow-derived rather than declared: a consumer that wants declarations only filters it out.' }, { table: 'dispatch_candidates', column: 'basis', value: 'mro', languages: P, meaning: 'The subtype\'s C3 linearisation picks the candidate for that attribute name. Not merely "the subtype declares this name" — a name a sibling base wins is attributed to that sibling.' }, @@ -764,7 +765,6 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'csharp', table: 'skipped', note: 'EMPTY — the C# front end writes no skipped-files report. It takes the opposite line: a construct its grammar does not cover fails the run rather than skipping the file, so there is no per-file decision to record. An empty table here is not evidence that every file was read.' }, { language: 'csharp', table: 'type_instantiated', note: 'EMPTY — the C# engine derives the instantiation set but does not export it. Dispatch in C# is not narrowed by it either (see the README of the C# engine).' }, { language: 'csharp', table: 'type_use', note: 'EMPTY — the C# front end does not export type uses yet.' }, - { language: 'csharp', table: 'overrides', note: 'EMPTY — C# records the dispatch envelope in dispatch_candidates, not as override rows.' }, { language: 'javascript', table: 'dispatch_candidates', note: 'EMPTY — JavaScript has no declared dispatch to widen from: a multi_inferred set is already the values the receiver may hold (see call_edges), so there is no envelope to record.' }, { language: 'javascript', table: 'skipped', note: 'A DIRECTORY_EXCLUDED row\'s file_path is a PRUNED DIRECTORY, not a file, and `detail` carries how many files are behind it; those files have no rows of their own. So `SELECT count(*) FROM skipped` is not the number of files missing, and a join on file_path will not match them. Filter the reason out when you want per-file rows.' }, { language: 'python', table: 'skipped', note: 'The only front end that positions a skip: a PY2_CONSTRUCT_DETECTED row carries the construct and its line and column, so the file can be triaged without re-running the parser.' }, diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl index 6e74254c7..fce505f12 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -178,6 +178,31 @@ call_chain_edge(e, caller, "-", callee, "lib", "boundary_lib", kind) :- call_from("client", e, caller), invocation_site("client", e, kind). +// client -> THE BODILESS DECLARATION A FANNED CALL BINDS TO. `s.Area()` on an +// IShape resolves to every implementation, and the declaration it is written +// against -- the interface member, or an abstract method -- is in no resolved set, +// because no body of it can run. Without this edge the declaration has no caller: +// asking who depends on IShape.Area finds nothing while every caller of it is in the +// graph. It is what the compiler binds the call to, and it takes the site's own class, +// so the site keeps the tier its runtime targets gave it. Emitted here and not in +// expr_resolves_to_method, which is the fan the oracle scores and the count the tier +// is read from. +call_chain_edge(e, caller, "-", declared, "client", cls, kind) :- + call_names_bodiless(e, declared), + call_from("client", e, caller), + call_class("client", e, cls), + invocation_site("client", e, kind). +call_names_bodiless(e, declared) :- + expr_call_candidate("client", e, declared), + method_decl("client", _, _, _, declared), + method_is_bodiless("client", declared), + !dispatch_is_exact("client", e), + !ovl_drop("client", e, declared), + expr_resolves_to_method("client", e, _). +method_is_bodiless(prov, m) :- method_is_abstract(prov, m). +method_is_bodiless(prov, m) :- + interface_member(prov, _, _, "method", m), !interface_has_default_impl(prov, m). + // client -> EXTERNAL. The target is outside every staged IR, so ToMethod is a LABEL // (`external:.`) rather than a method hash, and the bundle routes it to // callee_label by the "external" provenance -- the same convention Java and Python diff --git a/graph/csharp/engine/resolution/type-hierarchy.dl b/graph/csharp/engine/resolution/type-hierarchy.dl index 2d5f70aab..a88dfd533 100644 --- a/graph/csharp/engine/resolution/type-hierarchy.dl +++ b/graph/csharp/engine/resolution/type-hierarchy.dl @@ -223,6 +223,15 @@ type_ancestor("client", gk, agk) :- type_self_or_ancestor(prov, gk, gk) :- type_group(prov, _, gk). type_self_or_ancestor(prov, gk, agk) :- type_ancestor(prov, gk, agk). +// type_ancestor_type(Prov, TypeHash, AncestorTypeHash) -- type_ancestor keyed on the +// DECLARATION rather than the merged group, which is what the exported graph keys a +// type on: `types.id`, `methods.owner_type_id`. A partial type has one row per part, +// so every part is a subtype of every ancestor of the whole. The ancestor keeps its +// own provenance, so a client type deriving from a staged library type joins to the +// library's declaration. +type_ancestor_type(prov, t, at) :- + type_ancestor(prov, gk, agk), type_group(prov, t, gk), type_group(_, at, agk). + // type_subtype(Prov, Sub, Super) -- the reverse direction, which is what CHA fan-out // needs: given a declared receiver type, every type that could be there at runtime. type_subtype(prov, sub, super) :- type_ancestor(prov, sub, super). diff --git a/graph/csharp/engine/resolution/virtual-dispatch.dl b/graph/csharp/engine/resolution/virtual-dispatch.dl index 746fc3f4e..a429165f0 100644 --- a/graph/csharp/engine/resolution/virtual-dispatch.dl +++ b/graph/csharp/engine/resolution/virtual-dispatch.dl @@ -145,6 +145,17 @@ dispatch_target("client", ifaceM, impl, "interface_impl") :- implements_member(" dispatch_target(prov, ifaceM, ifaceM, "default_interface_impl") :- interface_has_default_impl(prov, ifaceM). +// THE DECLARED HALF OF THE ENVELOPE. override_pair(Base, Candidate) -- an override +// or an interface implementation in client code, which is what a call bound to Base +// may run instead. Without it a consumer asking what must change with an interface +// method found nothing: the call edges name the implementations, and nothing named +// the interface member they implement. Exported as the override table and as the +// `nominal` basis of the envelope, the shape Java gives the same pair. +override_pair(m, d) :- + dispatch_target(_, m, d, r), d != m, (r = "override" ; r = "interface_impl"), + method_decl("client", _, _, _, d). +method_dispatch_candidate(m, d, "nominal") :- override_pair(m, d). + // An override is also a target through everything IT overrides, transitively, which // the closure above gives. What it must NOT pick up is a HIDING method. dispatch_excluded_hiding(prov, m, h) :- diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 27a5ce721..4bd35c734 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -95,6 +95,7 @@ .decl call_kind(c0:symbol,c1:symbol,c2:symbol) .decl call_member_dispatches(c0:symbol,c1: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) .decl call_needs_member_lookup(c0:symbol,c1:symbol) .decl call_needs_scope_lookup(c0:symbol,c1:symbol) @@ -449,6 +450,7 @@ .decl method_in_type(c0:symbol,c1:symbol,c2:symbol) .decl method_is_abstract(c0:symbol,c1:symbol) .decl method_is_accessor(c0:symbol,c1:symbol) +.decl method_is_bodiless(c0:symbol,c1:symbol) .decl method_is_ctor(c0:symbol,c1:symbol) .decl method_is_dispatchable(c0:symbol,c1:symbol) .decl method_is_extension(c0:symbol,c1:symbol) @@ -536,6 +538,7 @@ .decl ns_prefix(c0:symbol,c1:symbol,c2:symbol) .decl override_cand(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_depth(c0:symbol,c1:symbol,c2:symbol,c3:number) +.decl override_pair(c0:symbol,c1:symbol) .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) @@ -689,6 +692,7 @@ .decl tok_at_a(c0:symbol,c1:number) .decl type_access(c0:symbol,c1:symbol,c2:symbol) .decl type_ancestor(c0:symbol,c1:symbol,c2:symbol) +.decl type_ancestor_type(c0:symbol,c1:symbol,c2:symbol) .decl type_arg_fixed(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl type_arg_for(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_assembly(c0:symbol,c1:symbol,c2:symbol) diff --git a/graph/csharp/souffle/export_manifest.tsv b/graph/csharp/souffle/export_manifest.tsv index d5c935d9c..1b4583e2c 100644 --- a/graph/csharp/souffle/export_manifest.tsv +++ b/graph/csharp/souffle/export_manifest.tsv @@ -36,3 +36,5 @@ remote_unsent remote-unsent.csv remote_undetermined remote-undetermined.csv entry_point entry-point.csv entry_reachable entry-reachable.csv +type_ancestor_type resolution-type-ancestor-type.csv +override_pair resolution-virtual-override.csv diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl index e8a88fb13..c60de8a6b 100644 --- a/graph/java/engine/resolution/external-types.dl +++ b/graph/java/engine/resolution/external-types.dl @@ -73,10 +73,16 @@ simple_name_declared(n) :- lib_type(n, _, _, _, _, _, _, _, _, _, _, _, _, _). name_resolves_in_file(tn, fp) :- type_resolves_in_file(_, tn, fp, _). name_resolves_in_file(tn, fp) :- ref_file(_, tn, fp), type_resolves_java_lang(_, tn, _). +// `var` is not a type name (JLS 3.9, 14.4). The parser records `var h = r.lookup(k)` as a +// LOCAL_VARIABLE reference written `var` that nothing resolves, so without this guard every call +// 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. candidate_ref(ref, tn, ctn, fp) :- java_type_reference(kind, ctx, _, _, _, _, _, "0", tn, ctn, _, _, _, _, _, _, _, ref), ext_ref_kind(kind), ext_ref_context(ctx), tn != "", + tn != "var", ref_file(ref, tn, fp), !name_resolves_in_file(tn, fp), !simple_name_declared(tn). @@ -149,6 +155,16 @@ expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, nam java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), !local_creation_pinned(local), local_var_type_external(local, x). +// A `var` local's static type IS its initializer's (JLS 14.4.1), fixed at the declaration, so a +// reassignment does not widen it. When that initializer is typed external (`var f = source()` +// 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), + local_init_expr(local, init), + expr_type("external", init, x). expr_type("external", e, x) :- java_expression("CAST_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), cast_type_external(e, x). // A chained receiver whose callee is a client method declared to return the external type. diff --git a/graph/java/engine/resolution/reference-types.dl b/graph/java/engine/resolution/reference-types.dl index 92d9bdb60..702199541 100644 --- a/graph/java/engine/resolution/reference-types.dl +++ b/graph/java/engine/resolution/reference-types.dl @@ -39,7 +39,8 @@ field_type_resolves(prov, field, ref, t) :- java_type_reference(_, "FIELD_TYPE", // position-0 references, so each caught type is still captured. local_var_type_resolves(prov, local, ref, t) :- java_type_reference(_, "LOCAL_VARIABLE", _, _, _, _, _, "0", _, _, _, _, _, _, _, local, "LOCAL_VARIABLE", ref), type_ref_resolves(prov, ref, t). -// A `var` local has no written type reference, so the rule above yields nothing. Infer +// A `var` local's only reference is written `var` and resolves to nothing, so the rule above +// yields nothing (and external-types.dl keeps it from naming an external type). Infer // the type from a direct object-creation initializer (var x = new Holder()): the // created type — carrying its written type arguments — becomes the local's resolved type, // which feeds callee resolution AND type-argument binding uniformly. Gated on the isVar diff --git a/graph/javascript/README.md b/graph/javascript/README.md index b3595b612..6a26b2abb 100644 --- a/graph/javascript/README.md +++ b/graph/javascript/README.md @@ -191,8 +191,10 @@ constructor), `TYPE_ONLY_TARGET` (the compiler named a JSDoc function type), ## What is not built -* **JSX component calls do not exist in the IR.** The JavaScript schema has no JSX call - kind, so `` is neither a site nor a miss; the oracle counts and excludes them. +* **JSX components reached through a wrapper are not followed.** `` is an edge + (kind `JSX_ELEMENT`, call-edge-generation/jsx.dl) and its attributes are the props the + component receives; a tag whose value is `memo(C)`, `connect(…)(C)` or another unstaged + library's return stays `ambiguous_unknown`. The oracle still excludes JSX sites. * **Primitive results are not typed.** `this.greet().toUpperCase()` is `ambiguous_unknown`, not `ambient_terminal`: nothing here knows `greet` returns a string. diff --git a/graph/javascript/engine/call-edge-generation/jsx.dl b/graph/javascript/engine/call-edge-generation/jsx.dl new file mode 100644 index 000000000..9a63833e7 --- /dev/null +++ b/graph/javascript/engine/call-edge-generation/jsx.dl @@ -0,0 +1,84 @@ +// ============================================================================ +// CALL-EDGE-GEN · JSX ELEMENTS (`` renders Component) +// +// A JSX element whose tag is a reference — ``, `` — is a call +// of that component by the renderer: a function component runs with the element's +// props as its first argument; a class component is constructed with them and its +// `render` runs. The parser emits the element as a JSX_ELEMENT expression with the +// tag under JSX_TAG_NAME and each attribute as a JSX_ATTRIBUTE_VALUE row (role +// PROPERTY_KEY, name = the prop) whose PROPERTY_VALUE child is the value. No +// js_call_site row exists for it, so before this file the element was neither an +// edge nor a miss: `impact TodoList` answered "nothing uses it". +// +// An INTRINSIC tag (`
`, ``) is JSX_INTRINSIC_ELEMENT and has no +// tag reference: it names no project code and gets no row here. +// +// Two effects, both through the ordinary value layer: +// 1. an edge per element: call_chain_edge(Element, Caller, -, Component, …, +// "JSX_ELEMENT"), tiered like a call (one target known_edge, several +// multi_inferred); a tag with no value is ambiguous_unknown — a declared miss, +// never silence. +// 2. the element's attributes are an object value keyed by the element, and it is +// what the component receives: `` makes +// `onToggle(todo)` inside `TodoItem({ onToggle })` resolve to handleToggle. +// Brace children are the `children` prop (a render prop is a function). +// +// Not modelled: a component reached only through a library wrapper +// (`memo(C)`, `connect(…)(C)`, `forwardRef(fn)`) whose library is not staged — the +// tag's value is the wrapper's return, which is unknown, and the element stays an +// ambiguous_unknown row. +// ============================================================================ + +// ── the element and what its tag holds ────────────────────────────────────── +jsx_element_tag(je, tag) :- expr_kind(_, "JSX_ELEMENT", _, je), expr_child(_, je, "JSX_TAG_NAME", _, tag). +jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, k, i). +// A CommonJS module value renders through its default export, as a call of it does. +jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, "module", mod), + module_export_value(mod, "default", k, i), k != "module". + +// ── jsx_renders(Element, Component) — the callables the element runs ─────── +// A function component (a constructor FUNCTION is callable_value's "ctor" half). +jsx_renders(je, m) :- jsx_tag_value(je, k, i), callable_value(k, i, m). +// A class component: constructed, then rendered. +jsx_renders(je, m) :- jsx_tag_value(je, "ctor", t), type_construct_target(t, m). +jsx_renders(je, m) :- jsx_tag_value(je, "ctor", t), type_member(t, "render", m). + +// ── the props object ──────────────────────────────────────────────────────── +// Keyed by the element, as an object literal is keyed by the literal. +prop_value("obj", je, n, k, i) :- expr_kind(_, "JSX_ELEMENT", _, je), + expr_child(_, je, "PROPERTY_KEY", _, a), expr_kind(_, "JSX_ATTRIBUTE_VALUE", _, a), expr_name(_, n, a), + expr_child(_, a, "PROPERTY_VALUE", _, v), expr_value(v, k, i). +// `` hands over every property of `rest`. +prop_value("obj", je, n, k, i) :- expr_kind(_, "JSX_ELEMENT", _, je), + expr_child(_, je, "SPREAD_OPERAND", _, o), expr_value(o, k0, i0), prop_value(k0, i0, n, k, i). +// `{(x) => …}` — a brace child is `props.children`. +prop_value("obj", je, "children", k, i) :- expr_kind(_, "JSX_ELEMENT", _, je), + expr_child(_, je, "OPERAND", _, c), expr_value(c, k, i). + +// A function component's first parameter receives the props — client functions only, +// and not a fan-capped one, exactly as argument flow at a call. +param_value(p, "obj", je) :- jsx_tag_value(je, k, i), k != "ctor", callable_value(k, i, m), + method_decl("client", _, _, _, _, _, _, m), !method_is_hot(m, _), + param_decl(_, _, "0", m, p), !param_is_rest(_, p). +// A class component: its constructor receives them, and `this.props` holds them. +param_value(p, "obj", je) :- jsx_tag_value(je, "ctor", t), type_construct_target(t, m), + method_decl("client", _, _, _, _, _, _, m), !method_is_hot(m, _), + param_decl(_, _, "0", m, p), !param_is_rest(_, p). +prop_value("inst", t, "props", "obj", je) :- jsx_tag_value(je, "ctor", t), type_decl("client", _, _, _, _, t). + +// ── the rows ──────────────────────────────────────────────────────────────── +// The caller is the callable whose body holds the markup; top-level markup +// (`root.render()`) belongs to the module initializer, as a top-level call does. +jsx_caller(je, m) :- jsx_element_tag(je, _), expr_owner(_, m, _, je), m != "". +jsx_caller(je, init) :- jsx_element_tag(je, _), expr_owner(_, "", mod, je), module_init(_, init, mod). +jsx_target_count(je, n) :- jsx_element_tag(je, _), n = count : { jsx_renders(je, _) }. +jsx_class(je, "known_edge") :- jsx_target_count(je, 1). +jsx_class(je, "multi_inferred") :- jsx_target_count(je, n), n >= 2. + +call_chain_edge(je, caller, "-", m, "client", cls, "JSX_ELEMENT") :- jsx_renders(je, m), + method_prov(m, "client"), jsx_caller(je, caller), jsx_class(je, cls). +call_chain_edge(je, caller, "-", m, "lib", "boundary_lib", "JSX_ELEMENT") :- jsx_renders(je, m), + method_prov(m, "lib"), jsx_caller(je, caller). +// A tag that names something the value layer could not follow: a declared unknown. +call_chain_edge(je, caller, "-", "-", "-", "ambiguous_unknown", "JSX_ELEMENT") :- + jsx_target_count(je, 0), jsx_caller(je, caller). diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index c4ad3ef53..6a91bfde3 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -42,6 +42,14 @@ .decl accessor_target_count(c0:symbol, c1:symbol, c2:number) .decl accessor_class(c0:symbol, c1:symbol, c2:symbol) +// ── call-edge-generation/jsx.dl ── +.decl jsx_element_tag(c0:symbol, c1:symbol) +.decl jsx_tag_value(c0:symbol, c1:symbol, c2:symbol) +.decl jsx_renders(c0:symbol, c1:symbol) +.decl jsx_caller(c0:symbol, c1:symbol) +.decl jsx_target_count(c0:symbol, c1:number) +.decl jsx_class(c0:symbol, c1:symbol) + // ── call-edge-generation/calls.dl ── .decl method_prov(c0:symbol, c1:symbol) .decl client_calls_client(c0:symbol, c1:symbol, c2:symbol, c3:symbol) diff --git a/graph/javascript/souffle/export_manifest.tsv b/graph/javascript/souffle/export_manifest.tsv index b4dbc3496..b90de230f 100644 --- a/graph/javascript/souffle/export_manifest.tsv +++ b/graph/javascript/souffle/export_manifest.tsv @@ -40,3 +40,4 @@ site_in_partial_module site-in-partial-module.csv package_entry package-entry.csv import_staged_package_unreached import-staged-package-unreached.csv member_write_refused member-write-refused.csv +jsx_renders jsx-renders.csv diff --git a/graph/test/csharp/tools/overload-by-argument-test.sh b/graph/test/csharp/tools/overload-by-argument-test.sh index 875ca8f77..a486731ec 100755 --- a/graph/test/csharp/tools/overload-by-argument-test.sh +++ b/graph/test/csharp/tools/overload-by-argument-test.sh @@ -68,8 +68,8 @@ Pruned G known_edge MathUtil.G(int) Pruned G known_edge MathUtil.G(T) Dispatched Area multi_inferred Circle.Area(int) Shape.Area(int) Dispatched Area multi_inferred Circle.Area(string) Shape.Area(string) -ThroughInterface K multi_inferred Explicit.K(int) Implicit.K(int) -ThroughInterface K multi_inferred Explicit.K(string) Implicit.K(string) +ThroughInterface K multi_inferred Explicit.K(int) IKeyed.K(int) Implicit.K(int) +ThroughInterface K multi_inferred Explicit.K(string) IKeyed.K(string) Implicit.K(string) Arity Pad known_edge MathUtil.Pad(int) Arity Pad known_edge MathUtil.Pad(int,int) Unknown Scale multi_inferred MathUtil.Scale(double) MathUtil.Scale(int) MathUtil.Scale(string) diff --git a/graph/test/java/cases/61-var-local-type/src/probe/Handler.java b/graph/test/java/cases/61-var-local-type/src/probe/Handler.java new file mode 100644 index 000000000..bffaafc98 --- /dev/null +++ b/graph/test/java/cases/61-var-local-type/src/probe/Handler.java @@ -0,0 +1,5 @@ +package probe; + +public interface Handler { + void handle(); +} diff --git a/graph/test/java/cases/61-var-local-type/src/probe/Registry.java b/graph/test/java/cases/61-var-local-type/src/probe/Registry.java new file mode 100644 index 000000000..195d1eb6b --- /dev/null +++ b/graph/test/java/cases/61-var-local-type/src/probe/Registry.java @@ -0,0 +1,7 @@ +package probe; + +public class Registry { + public Handler lookup(String key) { + return null; + } +} diff --git a/graph/test/java/cases/61-var-local-type/src/probe/VarLocals.java b/graph/test/java/cases/61-var-local-type/src/probe/VarLocals.java new file mode 100644 index 000000000..bf5c79e01 --- /dev/null +++ b/graph/test/java/cases/61-var-local-type/src/probe/VarLocals.java @@ -0,0 +1,110 @@ +package probe; + +import java.text.Format; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * `var` is a reserved type name (JLS 14.4), so a local declared with it never has a type called + * `var`: its type is the initializer's. Each construct below has a control written with the + * declared type, which must resolve the same way. + */ +public class VarLocals { + + /** var from a client call's return: one known edge, no library label. */ + void fromCall(Registry r) { + var h = r.lookup("x"); + h.handle(); + } + + /** control: the same call with the type written down. */ + void fromCallDeclared(Registry r) { + Handler h = r.lookup("x"); + h.handle(); + } + + /** var from a static factory. */ + void fromFactory() { + var w = Widget.create(); + w.paint(); + } + + /** control: var from `new` was already pinned to the created type. */ + void fromNew() { + var w = new Widget(); + w.paint(); + } + + /** var from `new`, then reassigned: no longer pinned, still a Widget. */ + void fromNewReassigned() { + var w = new Widget(); + w = Widget.create(); + w.paint(); + } + + /** var from a literal: String's own method, and nothing named var. */ + int fromLiteral() { + var s = "abc"; + return s.length(); + } + + /** control: the literal itself, with no local; a var must answer the same way. */ + int literalInline() { + return "abc".length(); + } + + /** control: the declared String. */ + int fromLiteralDeclared() { + String s = "abc"; + return s.length(); + } + + /** var loop variable over a platform list (not staged): unresolved, not a library call. */ + void fromEnhancedFor(List ws) { + for (var x : ws) { + x.paint(); + } + } + + /** control: the declared loop variable. */ + void fromEnhancedForDeclared(List ws) { + for (Widget x : ws) { + x.paint(); + } + } + + /** var resource in try-with-resources. */ + void fromResource() { + try (var w = Widget.create()) { + w.paint(); + } + } + + /** var from an unstaged platform call: the type is unknown, so the call stays unresolved. */ + int fromUnknown() { + var xs = Collections.emptyList(); + return xs.size(); + } + + /** control: a declared external local still types its receiver as that external type. */ + String declaredExternal(Object o) { + Format f = source(); + return f.format(o); + } + + /** var from a client call declared to return an external type: that type, not `var`. */ + String varExternal(Object o) { + var f = source(); + return f.format(o); + } + + Format source() { + return null; + } + + /** var lambda parameters (Java 11). */ + BiConsumer lambdaParams() { + return (var a, var b) -> a.paint(); + } +} diff --git a/graph/test/java/cases/61-var-local-type/src/probe/Widget.java b/graph/test/java/cases/61-var-local-type/src/probe/Widget.java new file mode 100644 index 000000000..11ded4520 --- /dev/null +++ b/graph/test/java/cases/61-var-local-type/src/probe/Widget.java @@ -0,0 +1,14 @@ +package probe; + +public class Widget implements AutoCloseable { + public static Widget create() { + return new Widget(); + } + + public void paint() { + } + + @Override + public void close() { + } +} diff --git a/graph/test/java/expected/36-generic-two-hop.edges b/graph/test/java/expected/36-generic-two-hop.edges index 6322a7de8..dbfa17bee 100644 --- a/graph/test/java/expected/36-generic-two-hop.edges +++ b/graph/test/java/expected/36-generic-two-hop.edges @@ -5,16 +5,12 @@ boundary_lib method probe.TwoHop#nestedArgOnLibRef() -> dep.Lookup#entries() boundary_lib method probe.TwoHop#nestedArgOnLibRef() -> dep.Pair#value() boundary_lib method probe.TwoHop#oneHop() -> dep.Box#get(int) boundary_lib method probe.TwoHop#oneHopVar() -> dep.Box#get(int) -boundary_lib method probe.TwoHop#oneHopVar() -> external:var.get boundary_lib method probe.TwoHop#twoHopBothVars() -> dep.Box#iterator() boundary_lib method probe.TwoHop#twoHopBothVars() -> dep.Iter#next() -boundary_lib method probe.TwoHop#twoHopBothVars() -> external:var.iterator -boundary_lib method probe.TwoHop#twoHopBothVars() -> external:var.next boundary_lib method probe.TwoHop#twoHopChain() -> dep.Box#iterator() boundary_lib method probe.TwoHop#twoHopChain() -> dep.Iter#next() boundary_lib method probe.TwoHop#twoHopVar() -> dep.Box#iterator() boundary_lib method probe.TwoHop#twoHopVar() -> dep.Iter#next() -boundary_lib method probe.TwoHop#twoHopVar() -> external:var.next known_edge method probe.TwoHop#nestedArgOnClientRef() -> probe.TwoHop#pairs() known_edge method probe.TwoHop#nestedArgOnClientRef() -> probe.TwoHop.Widget#id() known_edge method probe.TwoHop#nestedArgOnLibRef() -> probe.TwoHop#lookup() diff --git a/graph/test/java/expected/61-var-local-type.edges b/graph/test/java/expected/61-var-local-type.edges new file mode 100644 index 000000000..dcbfc0084 --- /dev/null +++ b/graph/test/java/expected/61-var-local-type.edges @@ -0,0 +1,25 @@ +ambiguous_unknown method probe.VarLocals#fromEnhancedFor(List) -> - +ambiguous_unknown method probe.VarLocals#fromLiteral() -> - +ambiguous_unknown method probe.VarLocals#fromUnknown() -> - +ambiguous_unknown method probe.VarLocals#lambdaParams() -> - +ambiguous_unknown method probe.VarLocals#literalInline() -> - +boundary_lib method probe.VarLocals#declaredExternal(Object) -> external:java.text.Format.format +boundary_lib method probe.VarLocals#fromLiteralDeclared() -> external:String.length +boundary_lib method probe.VarLocals#varExternal(Object) -> external:java.text.Format.format +known_edge method probe.VarLocals#declaredExternal(Object) -> probe.VarLocals#source() +known_edge method probe.VarLocals#fromCall(Registry) -> probe.Handler#handle() +known_edge method probe.VarLocals#fromCall(Registry) -> probe.Registry#lookup(String) +known_edge method probe.VarLocals#fromCallDeclared(Registry) -> probe.Handler#handle() +known_edge method probe.VarLocals#fromCallDeclared(Registry) -> probe.Registry#lookup(String) +known_edge method probe.VarLocals#fromEnhancedForDeclared(List) -> probe.Widget#paint() +known_edge method probe.VarLocals#fromFactory() -> probe.Widget#create() +known_edge method probe.VarLocals#fromFactory() -> probe.Widget#paint() +known_edge method probe.VarLocals#fromNew() -> probe.Widget#paint() +known_edge method probe.VarLocals#fromNewReassigned() -> probe.Widget#create() +known_edge method probe.VarLocals#fromNewReassigned() -> probe.Widget#paint() +known_edge method probe.VarLocals#fromResource() -> probe.Widget#create() +known_edge method probe.VarLocals#fromResource() -> probe.Widget#paint() +known_edge method probe.VarLocals#varExternal(Object) -> probe.VarLocals#source() +known_edge new probe.VarLocals#fromNew() -> probe.Widget#() +known_edge new probe.VarLocals#fromNewReassigned() -> probe.Widget#() +known_edge new probe.Widget#create() -> probe.Widget#() diff --git a/graph/test/java/expected/61-var-local-type.known-missing b/graph/test/java/expected/61-var-local-type.known-missing new file mode 100644 index 000000000..996b5ec5e --- /dev/null +++ b/graph/test/java/expected/61-var-local-type.known-missing @@ -0,0 +1,13 @@ +# Accepted gap, measured by the bytecode oracle in CI: a `var` local whose type comes from something +# other than its initializer expression is left a declared unknown. Before #1401 each of these was a +# wrong `external:var.` edge; now the receiver is honestly untyped. Three shapes: +# - the element variable of an enhanced for loop (`for (var w : widgets)`): its type is the +# iterable's element type; +# - a try-with-resources variable (`try (var w = open())` where open() is not typed client-side); +# - `var` lambda parameters (`(var w) -> w.paint()`): the type comes from the target functional +# interface. +# Follow-up: type `var` in these three shapes. It cannot rot: when one starts resolving, this line +# reports NOW-FIXED and the case fails until it is removed. +probe.VarLocals#fromEnhancedFor -> probe.Widget#paint() +probe.VarLocals#fromResource -> probe.Widget#close() +probe.VarLocals#lambdaParams -> probe.Widget#paint() diff --git a/graph/test/java/expected/61-var-local-type.oracle b/graph/test/java/expected/61-var-local-type.oracle new file mode 100644 index 000000000..b29608363 --- /dev/null +++ b/graph/test/java/expected/61-var-local-type.oracle @@ -0,0 +1,4 @@ +oracle=20 engine=17 agree=17 missing=3 (known 3, NEW 0) extra=0 + known-missing probe.VarLocals#fromEnhancedFor -> probe.Widget#paint() + known-missing probe.VarLocals#fromResource -> probe.Widget#close() + known-missing probe.VarLocals#lambdaParams -> probe.Widget#paint() diff --git a/graph/test/java/expected/61-var-local-type.type-use b/graph/test/java/expected/61-var-local-type.type-use new file mode 100644 index 000000000..e1b611ace --- /dev/null +++ b/graph/test/java/expected/61-var-local-type.type-use @@ -0,0 +1,25 @@ +ambiguous_unknown ANNOTATION_TYPE 0 probe.Widget [ANNOTATION] -> - +ambiguous_unknown IMPLEMENTS_INTERFACE 0 probe.Widget [TYPE] -> - +ambiguous_unknown LOCAL_VARIABLE 0 probe.VarLocals#declaredExternal(Object) [LOCAL_VARIABLE] -> - +ambiguous_unknown LOCAL_VARIABLE 0 probe.VarLocals#fromLiteralDeclared() [LOCAL_VARIABLE] -> - +ambiguous_unknown METHOD_PARAM 0 probe.Registry#lookup(String) [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 probe.VarLocals#declaredExternal(Object) [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 probe.VarLocals#fromEnhancedFor(List) [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 probe.VarLocals#fromEnhancedForDeclared(List) [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 probe.VarLocals#varExternal(Object) [METHOD_PARAM] -> - +ambiguous_unknown METHOD_RETURN 0 probe.VarLocals#declaredExternal(Object) [METHOD] -> - +ambiguous_unknown METHOD_RETURN 0 probe.VarLocals#lambdaParams() [METHOD] -> - +ambiguous_unknown METHOD_RETURN 0 probe.VarLocals#source() [METHOD] -> - +ambiguous_unknown METHOD_RETURN 0 probe.VarLocals#varExternal(Object) [METHOD] -> - +known_edge LOCAL_VARIABLE 0 probe.VarLocals#fromCallDeclared(Registry) [LOCAL_VARIABLE] -> probe.Handler +known_edge LOCAL_VARIABLE 0 probe.VarLocals#fromEnhancedForDeclared(List) [LOCAL_VARIABLE] -> probe.Widget +known_edge METHOD_PARAM 0 probe.VarLocals#fromCall(Registry) [METHOD_PARAM] -> probe.Registry +known_edge METHOD_PARAM 0 probe.VarLocals#fromCallDeclared(Registry) [METHOD_PARAM] -> probe.Registry +known_edge METHOD_PARAM 1 probe.VarLocals#fromEnhancedFor(List) [METHOD_PARAM] -> probe.Widget +known_edge METHOD_PARAM 1 probe.VarLocals#fromEnhancedForDeclared(List) [METHOD_PARAM] -> probe.Widget +known_edge METHOD_RETURN 0 probe.Registry#lookup(String) [METHOD] -> probe.Handler +known_edge METHOD_RETURN 0 probe.Widget#create() [METHOD] -> probe.Widget +known_edge METHOD_RETURN 1 probe.VarLocals#lambdaParams() [METHOD] -> probe.Widget +known_edge OBJECT_CREATION_TYPE 0 probe.VarLocals#fromNew() [EXPRESSION] -> probe.Widget +known_edge OBJECT_CREATION_TYPE 0 probe.VarLocals#fromNewReassigned() [EXPRESSION] -> probe.Widget +known_edge OBJECT_CREATION_TYPE 0 probe.Widget#create() [EXPRESSION] -> probe.Widget diff --git a/graph/test/java/expected/oracle-agreement.txt b/graph/test/java/expected/oracle-agreement.txt index a204958ea..22a372c44 100644 --- a/graph/test/java/expected/oracle-agreement.txt +++ b/graph/test/java/expected/oracle-agreement.txt @@ -1,4 +1,4 @@ -cases compared 45 agreeing 45 +cases compared 46 agreeing 46 CONSTRUCTOR-RULE disagreements: 0 cases, 0 rows OTHER disagreements: 0 rows naming no method: 0 diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx new file mode 100644 index 000000000..538db49ad --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx @@ -0,0 +1,26 @@ +import { TodoList } from './components/TodoList.jsx'; +import Panel from './components/Panel.jsx'; +import * as ui from './components/ui.jsx'; + +// CONTROL: a project function spelled like an intrinsic tag. `
` is intrinsic and +// must not become an edge to it. +function div() { return 'not a component'; } + +// CONTROL: named only in a string, never rendered — no edge. +function Unused() { return null; } +const label = 'Unused'; + +export default function App({ title }) { + const extra = { onClose: closeAll }; + return ( +
+ + + + {(item) => format(item)} +
+ ); +} + +function closeAll() { return div(); } +function format(item) { return String(item); } diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx new file mode 100644 index 000000000..b3033b90a --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx @@ -0,0 +1,7 @@ +// A class component with no constructor, as most are: rendered, and `this.props` +// holds the attributes. +export default class Panel { + render() { + return
this.props.onClose()} />; + } +} diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx new file mode 100644 index 000000000..b74829bcd --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx @@ -0,0 +1,4 @@ +// Props arrive destructured; the onToggle prop is whatever the element passed. +export default function TodoItem({ todo, onToggle }) { + return
  • onToggle(todo)}>{todo.text}
  • ; +} diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx new file mode 100644 index 000000000..c09e7390a --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx @@ -0,0 +1,9 @@ +import TodoItem from './TodoItem.jsx'; + +export function TodoList({ heading }) { + function handleToggle(todo) { return save(todo); } + const todos = [{ text: heading }]; + return
      {todos.map((t) => )}
    ; +} + +function save(todo) { return todo; } diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx new file mode 100644 index 000000000..743dfa1a9 --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx @@ -0,0 +1,3 @@ +export function Badge(props) { return {props.count}; } +// A render prop: the brace child is props.children. +export function List(props) { return
      {[1, 2].map((x) => props.children(x))}
    ; } diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx new file mode 100644 index 000000000..cc5033bb0 --- /dev/null +++ b/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx @@ -0,0 +1,4 @@ +import App from './App.jsx'; +// Top-level markup belongs to the module initializer. +mount(); +function mount(el) { return el; } diff --git a/graph/test/javascript/expected/62-jsx-component-render.diag b/graph/test/javascript/expected/62-jsx-component-render.diag new file mode 100644 index 000000000..6073284d3 --- /dev/null +++ b/graph/test/javascript/expected/62-jsx-component-render.diag @@ -0,0 +1,4 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved App.jsx:26:32 FUNCTION_CALL String no_target +unresolved components/TodoList.jsx:6:15 METHOD_CALL map no_target +unresolved components/ui.jsx:3:43 METHOD_CALL map no_target diff --git a/graph/test/javascript/expected/62-jsx-component-render.edges b/graph/test/javascript/expected/62-jsx-component-render.edges new file mode 100644 index 000000000..05290ffbf --- /dev/null +++ b/graph/test/javascript/expected/62-jsx-component-render.edges @@ -0,0 +1,18 @@ +App.jsx:17:7 JSX_ELEMENT -> known_edge components/TodoList.jsx:3:1 TodoList +App.jsx:18:7 JSX_ELEMENT -> known_edge components/Panel.jsx:4:3 render +App.jsx:19:7 JSX_ELEMENT -> known_edge components/ui.jsx:1:1 Badge +App.jsx:20:27 FUNCTION_CALL format -> known_edge App.jsx:26:1 format +App.jsx:20:7 JSX_ELEMENT {(item) => format(item)} -> known_edge components/ui.jsx:3:1 List +App.jsx:25:30 FUNCTION_CALL div -> known_edge App.jsx:7:1 div +App.jsx:26:32 FUNCTION_CALL String -> ambient_terminal - +components/Panel.jsx:5:36 METHOD_CALL this.props.onClose -> known_edge App.jsx:25:1 closeAll +components/TodoItem.jsx:3:29 FUNCTION_CALL onToggle -> known_edge components/TodoList.jsx:4:3 handleToggle +components/TodoList.jsx:4:40 FUNCTION_CALL save -> known_edge components/TodoList.jsx:9:1 save +components/TodoList.jsx:6:15 METHOD_CALL todos.map -> ambient_terminal - +components/TodoList.jsx:6:15 METHOD_CALL todos.map -> callback_registered components/TodoList.jsx:6:25 +components/TodoList.jsx:6:32 JSX_ELEMENT -> known_edge components/TodoItem.jsx:2:1 TodoItem +components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> ambient_terminal - +components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> callback_registered components/ui.jsx:3:54 +components/ui.jsx:3:61 METHOD_CALL props.children -> known_edge App.jsx:20:17 +main.jsx:3:1 FUNCTION_CALL mount -> known_edge main.jsx:4:1 mount +main.jsx:3:7 JSX_ELEMENT -> known_edge App.jsx:13:1 App diff --git a/graph/test/javascript/expected/62-jsx-component-render.oracle b/graph/test/javascript/expected/62-jsx-component-render.oracle new file mode 100644 index 000000000..b79c5b4f1 --- /dev/null +++ b/graph/test/javascript/expected/62-jsx-component-render.oracle @@ -0,0 +1,8 @@ +App.jsx:20:27 FUNCTION_CALL format EXACT App.jsx:26:1 +App.jsx:25:30 FUNCTION_CALL div EXACT App.jsx:7:1 +App.jsx:26:32 FUNCTION_CALL String LIB_AMBIENT_OK +components/TodoList.jsx:4:40 FUNCTION_CALL save EXACT components/TodoList.jsx:9:1 +components/TodoList.jsx:6:15 METHOD_CALL map LIB_AMBIENT_OK +components/ui.jsx:3:43 METHOD_CALL map LIB_AMBIENT_OK +main.jsx:3:1 FUNCTION_CALL mount EXACT main.jsx:4:1 +# defects: 0 diff --git a/graph/test/javascript/tools/normalize_edges.py b/graph/test/javascript/tools/normalize_edges.py index c219389d2..8c515c322 100644 --- a/graph/test/javascript/tools/normalize_edges.py +++ b/graph/test/javascript/tools/normalize_edges.py @@ -51,7 +51,8 @@ def methods(d, tag): if h: i = {c: h.index(c) for c in ('jsExpressionUniqueHash', 'ownerModuleLinkHash', 'startLine', 'startColumn', 'expressionKind', 'text')} for r in exprs: - if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS'): + # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site. + if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT'): access[r[i['jsExpressionUniqueHash']]] = '%s:%s:%s %%s %s' % ( modfile.get(r[i['ownerModuleLinkHash']], '?'), r[i['startLine']], r[i['startColumn']], r[i['text']].replace('\n', ' ')[:60]) diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/__init__.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/__init__.py new file mode 100644 index 000000000..5c17aacf7 --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/__init__.py @@ -0,0 +1,7 @@ +"""The rows `impact` and `path` read out of framework_edge (#1509), in a package. + +The issue's own shape: every module imports its neighbours RELATIVELY, the signal and the +task live in their own modules, and each end sits in a different file. Cases 19 and 21 pin +the mechanisms; this one pins the exact rows the two query verbs consume, and the rows +that must NOT exist, because a consumer that reads framework_edge would print them. +""" diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/handlers.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/handlers.py new file mode 100644 index 000000000..a9321d30f --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/handlers.py @@ -0,0 +1,15 @@ +"""The receiver of order_placed, and `audit`, an ordinary callee beside it (the control +that keeps its resolved edge and gains no framework row).""" + +from django.dispatch import receiver + +from .signals import order_placed + + +@receiver(order_placed) +def on_placed(sender, **kwargs): + return sender + + +def audit(order): + return order diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/models.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/models.py new file mode 100644 index 000000000..3d9a57875 --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/models.py @@ -0,0 +1,12 @@ +"""The publisher: `place` sends the signal, `archive` calls `audit` directly.""" + +from .handlers import audit +from .signals import order_placed + + +class Order: + def place(self): + order_placed.send(sender=self.__class__) + + def archive(self): + audit(self) diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/producer.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/producer.py new file mode 100644 index 000000000..0965e78a8 --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/producer.py @@ -0,0 +1,21 @@ +"""Four `.delay` / `.apply_async` sites. Two join a registered task. `anim.delay` is an +ordinary method call, and `plain.delay` names a def no task decorator registered: neither +may produce a framework_edge row.""" + +from .tasks import Animation, plain, retry_report, send_report + + +def enqueue(key): + send_report.delay(key) + + +def reschedule(key): + retry_report.apply_async(args=[key], countdown=30) + + +def pause(anim: Animation): + anim.delay(10) + + +def run_plain(key): + plain.delay(key) diff --git a/graph/test/python/cases/29-framework-edge-consumers/src/app/signals.py b/graph/test/python/cases/29-framework-edge-consumers/src/app/signals.py new file mode 100644 index 000000000..bd3875071 --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/signals.py @@ -0,0 +1,5 @@ +"""The signal object, in its own module, imported by both ends.""" + +from django.dispatch import Signal + +order_placed = Signal() 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 new file mode 100644 index 000000000..4adaa970d --- /dev/null +++ b/graph/test/python/cases/29-framework-edge-consumers/src/app/tasks.py @@ -0,0 +1,23 @@ +"""Two registered tasks (one with bind=True), an unregistered function, and a class +whose method is also called `delay`.""" + +from celery import shared_task + + +@shared_task +def send_report(key): + return key + + +@shared_task(bind=True) +def retry_report(self, key): + return key + + +def plain(key): + return key + + +class Animation: + def delay(self, ms): + return ms diff --git a/graph/test/python/expected/29-framework-edge-consumers.edges b/graph/test/python/expected/29-framework-edge-consumers.edges new file mode 100644 index 000000000..b8a1c4da5 --- /dev/null +++ b/graph/test/python/expected/29-framework-edge-consumers.edges @@ -0,0 +1,12 @@ +ambiguous_unknown DECORATOR_APPLICATION app.handlers. -> - +ambiguous_unknown DECORATOR_APPLICATION app.tasks. -> - +ambiguous_unknown DECORATOR_BARE app.tasks. -> - +ambiguous_unknown METHOD_CALL app.models.Order.place -> - +ambiguous_unknown METHOD_CALL app.producer.enqueue -> - +ambiguous_unknown METHOD_CALL app.producer.reschedule -> - +ambiguous_unknown METHOD_CALL app.producer.run_plain -> - +boundary_lib DECORATOR_CALL app.handlers. -> external:receiver +boundary_lib DECORATOR_CALL app.tasks. -> external:shared_task +boundary_lib SIMPLE_CALL app.signals. -> external:Signal +known_edge METHOD_CALL app.producer.pause -> app.tasks.Animation.delay +known_edge SIMPLE_CALL app.models.Order.archive -> app.handlers.audit diff --git a/graph/test/python/expected/29-framework-edge-consumers.entries b/graph/test/python/expected/29-framework-edge-consumers.entries new file mode 100644 index 000000000..21c1275af --- /dev/null +++ b/graph/test/python/expected/29-framework-edge-consumers.entries @@ -0,0 +1,4 @@ +── entry_point (3) ── + signal_receiver app.handlers.on_placed app/handlers.py:10 + task app.tasks.retry_report app/tasks.py:13 + task app.tasks.send_report app/tasks.py:8 diff --git a/graph/test/python/expected/29-framework-edge-consumers.framework b/graph/test/python/expected/29-framework-edge-consumers.framework new file mode 100644 index 000000000..5c99eef84 --- /dev/null +++ b/graph/test/python/expected/29-framework-edge-consumers.framework @@ -0,0 +1,10 @@ +── framework_edge (3) ── + 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 + 2 task_dispatch delay +── remote_edge (0) ── +── remote_unserved (0) ── +── remote_unsent (0) ── diff --git a/graph/test/python/expected/29-framework-edge-consumers.tiers b/graph/test/python/expected/29-framework-edge-consumers.tiers new file mode 100644 index 000000000..3465118ce --- /dev/null +++ b/graph/test/python/expected/29-framework-edge-consumers.tiers @@ -0,0 +1,30 @@ +distinct call sites emitted: 12 + +--- by tier: edge ROWS, and the distinct SITES they cover --- + 7 rows 7 sites ambiguous_unknown + 3 rows 3 sites boundary_lib + 2 rows 2 sites known_edge + +--- edge rows by call kind --- + 2 DECORATOR_APPLICATION + 1 DECORATOR_BARE + 2 DECORATOR_CALL + 5 METHOD_CALL + 2 SIMPLE_CALL + +--- unresolved reasons --- + 2 decorator_factory_result_untyped + 1 unmodelled_decorator + 4 untyped_receiver:local_untyped + +--- the engine's own conservation ledger --- + 12 _total_sites + 7 ambiguous_unknown + 3 boundary_lib + 2 known_edge + +--- reconciling rows against the conserved site count --- + edge rows 12 + minus extra rows from multi-target sites 0 + = tier/site pairs 12 + engine's conserved site total 12 diff --git a/graph/test/typescript/cases/74-jsx-component-forms/src/app.tsx b/graph/test/typescript/cases/74-jsx-component-forms/src/app.tsx new file mode 100644 index 000000000..752806ab3 --- /dev/null +++ b/graph/test/typescript/cases/74-jsx-component-forms/src/app.tsx @@ -0,0 +1,24 @@ +import Avatar, { Badge, Field, lazy, Panel, UserCard, factory, ui } from "./parts"; + +// lazy() around a dynamic import: the component is the module's default export. +const Settings = lazy(() => import("./settings")); + +export function App(): string { + return ( +
    + + + + + + + +
    plain
    +
    + ); +} + +// CONTROL: the factory product is called, never its argument. +export function useFactory(): unknown { + return factory(); +} diff --git a/graph/test/typescript/cases/74-jsx-component-forms/src/parts.tsx b/graph/test/typescript/cases/74-jsx-component-forms/src/parts.tsx new file mode 100644 index 000000000..91fc610fe --- /dev/null +++ b/graph/test/typescript/cases/74-jsx-component-forms/src/parts.tsx @@ -0,0 +1,73 @@ +// The shapes a component is DECLARED in, each rendered from app.tsx as a JSX tag. +// `` is an invocation of X whatever X is bound to: a function, an arrow held by +// a const, a default export, a class, or a wrapper's result around one of those. +// +// No React dependency: the wrappers are declared here with the signatures that matter. +declare global { + namespace JSX { + type Element = string; + interface ElementAttributesProperty { + props: unknown; + } + interface IntrinsicElements { + [name: string]: unknown; + } + } +} + +export type Fc

    = (props: P) => string; +export declare function memo

    (component: Fc

    ): Fc

    ; +export declare function forwardRef

    (render: (props: P, ref: unknown) => string): Fc

    ; +export declare function lazy

    (load: () => Promise<{ default: Fc

    }>): Fc

    ; +export declare function makeFactory(build: () => T): () => T; + +export function formatName(n: string): string { + return n.toUpperCase(); +} + +export function trackClick(id: string): string { + return id; +} + +// memo() around a NAMED function expression. +export const UserCard = memo(function UserCard(p: { id: string }): string { + return formatName(p.id); +}); + +// forwardRef() around a function declared elsewhere, passed by name. +function FieldImpl(p: { label: string }, ref: unknown): string { + return trackClick(p.label) + String(ref); +} +export const Field = forwardRef(FieldImpl); + +// An arrow held by a typed const. +export const Badge: Fc<{ n: number }> = (p) => String(p.n); + +// A default export. +export default function Avatar(p: { src: string }): string { + return p.src; +} + +// A class component: the constructor is the nameable target. +export class Panel { + props: { title: string }; + constructor(props: { title: string }) { + this.props = props; + } + render(): string { + return this.props.title; + } +} + +export const ui = { + Chip(p: { text: string }): string { + return p.text; + }, +}; + +// CONTROL: a non-JSX wrapper. A factory's argument 0 is not what its product runs +// when called, so `factory()` must not become a call to buildDefault. +export function buildDefault(): number { + return 1; +} +export const factory = makeFactory(buildDefault); diff --git a/graph/test/typescript/cases/74-jsx-component-forms/src/settings.tsx b/graph/test/typescript/cases/74-jsx-component-forms/src/settings.tsx new file mode 100644 index 000000000..da11f44df --- /dev/null +++ b/graph/test/typescript/cases/74-jsx-component-forms/src/settings.tsx @@ -0,0 +1,7 @@ +export function saveSettings(): number { + return 1; +} + +export default function Settings(): string { + return String(saveSettings()); +} diff --git a/graph/test/typescript/cases/75-jsx-wrapper-guards/src/app.tsx b/graph/test/typescript/cases/75-jsx-wrapper-guards/src/app.tsx new file mode 100644 index 000000000..9a7c373c2 --- /dev/null +++ b/graph/test/typescript/cases/75-jsx-wrapper-guards/src/app.tsx @@ -0,0 +1,73 @@ +import { dynamic, lazy, makeThing, memo, observer, useMemoLike, type Fc } from "./parts"; +import { Button } from "./ui/Button"; + +export function factoryArgBody(): number { + return 3; +} +export function memoBody(): number { + return 4; +} +function ObservedImpl(): string { + return "observed"; +} + +// A loader that projects a NAMED export: the component is NamedHeavy, never Heavy. +const LazyNamed = lazy(() => import("./heavy").then((m) => ({ default: m.NamedHeavy }))); +const DynNamed = dynamic(() => import("./heavy").then((m) => m.NamedHeavy)); +// …and the same projections written with a block body, where the returned value is a row. +const LazyBlock = lazy(() => import("./heavy").then((m) => { return { default: m.NamedHeavy }; })); +const DynAwait = dynamic(async () => { const m = await import("./heavy"); return m.NamedHeavy; }); +// CONTROL: a loader returning the module itself renders its default export. +const LazyDefault = lazy(() => import("./heavy")); +const DynDefault = dynamic(() => import("./heavy")); + +// NOT wrappers: argument 0 is not what the product renders. +const Thing = makeThing(() => factoryArgBody()); +const Made = useMemoLike(() => function MadeInner(): string { return "made"; }, []); + +// CONTROL: real wrappers still reach their argument. +const Memoed = memo(function MemoInner(): string { return String(memoBody()); }); +const Observed = observer(ObservedImpl); + +export function Page(): string { + return ( +

    + + + + + + + + + + +
    + ); +} + +// A destructured prop SHADOWS the imported Button: none of these render ui/Button. +export function ShorthandTag({ Button }: { Button: Fc<{}> }): string { + return