From f85001fc04237939df904a9492865b252442cfce Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:01:00 -0700 Subject: [PATCH 1/3] fix(java): overload choice reads the argument types it was missing; param flow keeps only what a parameter can hold; an unqualified call stops at the innermost member - param-flow: an argument's type flowed into every same-name candidate's parameter, so a self-calling overload family gave every parameter all of its siblings' types and the overload choice kept every overload. A flowed type is kept where the parameter can hold it (declared type, a subtype, Object, an unresolved declared type, or a type whose hierarchy leaves the project against an unstaged one). - overload: without the JDK staged, a string concatenation, a class literal, a creation of an unstaged type, a catch parameter and a cast are typed; String is final, so a known non-String reference argument never fits a String parameter and a String argument never fits a client-typed one. - callee resolution: an unqualified call in a nested class searches outward only to the innermost type with a member of that name (JLS 15.12.1); the outer method it shadows is no candidate. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../callee-resolution.dl | 29 +++++++- .../engine/expression-resolution/overload.dl | 73 +++++++++++++++++++ .../java/engine/resolution/external-types.dl | 20 +++++ graph/java/engine/resolution/param-flow.dl | 30 +++++++- .../java/expected/29-qualified-this.edges | 3 +- .../java/expected/29-qualified-this.oracle | 3 +- .../case.json | 18 +++++ .../src/pkg/Node.java | 3 + .../src/pkg/Sink.java | 24 ++++++ .../src/pkg/User.java | 16 ++++ .../case.json | 14 ++++ .../src/pkg/Codec.java | 17 +++++ .../src/pkg/El.java | 3 + .../src/pkg/Leaf.java | 3 + .../src/pkg/Parser.java | 7 ++ .../src/pkg/Tok.java | 8 ++ .../case.json | 8 ++ .../src/pkg/Doc.java | 22 ++++++ 18 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 tests/cases/java/overload-argument-types-without-the-platform/case.json create mode 100644 tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Node.java create mode 100644 tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Sink.java create mode 100644 tests/cases/java/overload-argument-types-without-the-platform/src/pkg/User.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/case.json create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Codec.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/El.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Leaf.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Parser.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Tok.java create mode 100644 tests/cases/java/unqualified-call-stops-at-the-innermost-member/case.json create mode 100644 tests/cases/java/unqualified-call-stops-at-the-innermost-member/src/pkg/Doc.java diff --git a/graph/java/engine/expression-resolution/callee-resolution.dl b/graph/java/engine/expression-resolution/callee-resolution.dl index 3f4e4ad18..c101400c1 100644 --- a/graph/java/engine/expression-resolution/callee-resolution.dl +++ b/graph/java/engine/expression-resolution/callee-resolution.dl @@ -57,10 +57,37 @@ expr_call_candidate(call, callee) :- call_unqualified(call, name), // 26d7b166ba (5 of 5) was this shape. type_in_type (containment/type-nesting.dl) is already // transitive over enclosing types — its own header says it exists for exactly this purpose; // nothing consumed it. +// +// ...but only from the INNERMOST type that has a member of that name (JLS 15.12.1): the search stops +// at the first enclosing class declaring or inheriting the name, whatever its arity, so in +// `class Document { void charset(Charset c) {..} static class OutputSettings { OutputSettings +// charset(Charset c) {..} OutputSettings() { charset(UTF_8); } } }` the call is OutputSettings' +// own charset, never the outer Document.charset it shadows. expr_call_candidate(call, callee) :- call_unqualified(call, name), expr_ultimate_type("client", call, encl), type_in_type("client", encl, outer), - method_name_in_type(name, outer, callee). + method_name_in_type(name, outer, callee), + !unq_name_shadowed(call, outer). +// Read off the declarations and the hierarchy alone (a member declared in t or an ancestor), not +// method_name_in_type, which sits in the expr_type cycle this negation must stay out of. +.decl unq_member(name:symbol, t:symbol) +unq_member(name, t) :- java_method(name, _, _, _, _, _, _, t, _, _, _, _, _, _, _, _, _, _, _, _, _, _). +unq_member(name, t) :- type_ancestor(t, a), java_method(name, _, _, _, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, _, _, _). +unq_member(name, t) :- type_ancestor(t, a), lib_method(name, _, _, _, _, _, _, a, _, _, _, _, _, _, _, _, _, _, _, _, _, _). +.decl unq_name_found(call:symbol, t:symbol) +unq_name_found(call, encl) :- call_unqualified(call, name), + expr_ultimate_type("client", call, encl), + unq_member(name, encl). +unq_name_found(call, t) :- call_unqualified(call, name), + expr_ultimate_type("client", call, encl), + type_in_type("client", encl, t), + unq_member(name, t). +.decl unq_name_shadowed(call:symbol, outer:symbol) +unq_name_shadowed(call, outer) :- unq_name_found(call, encl), expr_ultimate_type("client", call, encl), + unq_name_found(call, outer), outer != encl. +unq_name_shadowed(call, outer) :- unq_name_found(call, t), unq_name_found(call, outer), + expr_ultimate_type("client", call, encl), t != encl, + type_in_type("client", t, outer). // ── Static-imported unqualified call (import static C.m; ... m(...)): a receiver-less // call whose name is NOT the enclosing instance's method but a static member imported diff --git a/graph/java/engine/expression-resolution/overload.dl b/graph/java/engine/expression-resolution/overload.dl index e312a933b..74dda5efd 100644 --- a/graph/java/engine/expression-resolution/overload.dl +++ b/graph/java/engine/expression-resolution/overload.dl @@ -363,6 +363,79 @@ value_mismatch(call, callee) :- expr_call_candidate(call, callee), !arg_is_target_typed(call, pos). // ── Combined: a candidate is contradicted by either regime ────────────────────── +// ── Argument types the regimes above could not see without the platform staged ───────────── +// (1) STRING CONCATENATION. `"cannot read " + name` is a String (JLS 15.18.1) whether or not the +// JDK is staged; expr-type.dl types it only through the staged java.lang.String, so with no JDK the +// argument had no type and every one-argument overload stayed (a message constructor taking a +// String and one taking a Throwable both kept). A `+` with a String literal operand, or a nested +// concatenation, is a String in the value regime. +.decl str_concat_expr(e:symbol) +str_concat_expr(e) :- java_expression("BINARY_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, "+", _, _, _, _, _, _, _, _, _, _, e), + java_expression("LITERAL", _, _, _, _, _, e, _, _, lt, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _), + literal_value_type(lt, "String"). +str_concat_expr(e) :- java_expression("BINARY_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, "+", _, _, _, _, _, _, _, _, _, _, e), + java_expression("BINARY_EXPRESSION", _, _, _, _, _, e, _, _, _, _, _, _, "+", _, _, _, _, _, _, _, _, _, _, inner), + str_concat_expr(inner). +str_concat_expr(e) :- java_expression("BINARY_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, "+", _, _, _, _, _, _, _, _, _, _, e), + java_expression("PARENTHESIZED", _, _, _, _, _, e, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, par), + java_expression(_, "PARENTHESIZED_INNER", _, _, _, _, par, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, inner), + str_concat_expr(inner). +arg_value_type(call, pos, "String") :- java_expression("BINARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, "+", _, _, _, _, _, _, _, _, _, _, e), + str_concat_expr(e). +// (1b) A CAST names its value's type: `append((char) c)` is a char, `put((String) v)` a String, +// whatever c and v are. A primitive cast target carries no type hash, so only the value regime +// can read it. +arg_value_type(call, pos, vt) :- java_expression("CAST_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + java_type_reference(_, "CAST_EXPRESSION", _, _, _, _, _, "0", vt, _, _, _, _, _, _, e, "EXPRESSION", _), + value_type_name(vt). +// (2) A CLASS LITERAL is a java.lang.Class. With the JDK staged expr_type says so; without it the +// argument is the external Class node the parameters `Class type` name, so `fromJson(json, +// User.class)` keeps fromJson(String, Class) and drops fromJson(String, Type) and the overloads +// taking a client type (the exact-external and client clauses above). +.decl class_ext(x:symbol) +class_ext(x) :- external_type(x), x = "external:Class". +class_ext(x) :- external_type(x), x = "external:java.lang.Class". +arg_type_hash(call, pos, x) :- java_expression("CLASS_LITERAL", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _), + !lib_type(_, "java.lang.Class", _, _, _, _, _, _, _, _, _, _, _, _), + class_ext(x). +// (3) `new X(..)` of a type no staged IR declares is an X: the external node of the creation's own +// type reference (resolution/external-types.dl), as for a parameter or a local declared X. +arg_type_hash(call, pos, x) :- java_expression("OBJECT_CREATION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e), + java_type_reference(_, "OBJECT_CREATION_TYPE", _, _, _, _, _, "0", _, _, _, _, _, _, _, e, "EXPRESSION", ref), + type_ref_external(ref, x). +// (4) java.lang.String is final: only a String (or null) is passed to a String parameter. A String +// argument never fits a client-typed parameter, and an argument whose static type is known to be a +// reference type other than String never fits a String parameter. Known means read off a +// declaration or the expression itself (a variable, a creation, a cast, a class literal), never a +// call's result, whose generic return the engine may only have erased. +.decl arg_static_ref(call:symbol, pos:symbol) +arg_static_ref(call, pos) :- java_expression("IDENTIFIER_REFERENCE", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, k, _, _, _, _, _, _, _, _, _, _), + static_ref_kind(k). +arg_static_ref(call, pos) :- java_expression(k, "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _), + static_expr_kind(k). +.decl static_ref_kind(k:symbol) +static_ref_kind("LOCAL_VARIABLE"). static_ref_kind("PARAMETER"). static_ref_kind("FIELD"). +.decl static_expr_kind(k:symbol) +static_expr_kind("OBJECT_CREATION"). static_expr_kind("CAST_EXPRESSION"). static_expr_kind("CLASS_LITERAL"). +.decl string_type_id(t:symbol) +string_type_id(t) :- str_type_hash(t). +string_type_id("external:String"). +string_type_id("external:java.lang.String"). +.decl arg_may_be_string(call:symbol, pos:symbol) +arg_may_be_string(call, pos) :- arg_type_hash(call, pos, t), string_type_id(t). +arg_may_be_string(call, pos) :- arg_value_type(call, pos, "String"). +arg_may_be_string(call, pos) :- arg_value_type(call, pos, "null"). +value_mismatch(call, callee) :- expr_call_candidate(call, callee), + param_value_type(callee, pos, "String"), + arg_static_ref(call, pos), + arg_type_hash(call, pos, _), + !arg_may_be_string(call, pos), + !arg_is_target_typed(call, pos). +value_mismatch(call, callee) :- expr_call_candidate(call, callee), + arg_value_type(call, pos, "String"), + param_type_hash(callee, pos, pt), type_prov(pt, "client"), + !arg_is_target_typed(call, pos). + arg_mismatch(call, callee) :- hash_mismatch(call, callee). arg_mismatch(call, callee) :- value_mismatch(call, callee). diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl index 46427be85..3a64e9400 100644 --- a/graph/java/engine/resolution/external-types.dl +++ b/graph/java/engine/resolution/external-types.dl @@ -52,6 +52,8 @@ ext_ref_context("LOCAL_VARIABLE"). ext_ref_context("METHOD_PARAM"). ext_ref_context("METHOD_RETURN"). ext_ref_context("CAST_EXPRESSION"). +// a creation of an unstaged type: its value is that type (an overload argument, overload.dl) +ext_ref_context("OBJECT_CREATION_TYPE"). // "Unresolved" is decided by the resolver's own FILE-SCOPED answer, not by negating // type_ref_resolves: that relation sits in one stratum with the hierarchy (a library @@ -128,6 +130,24 @@ type_parent(sub, x) :- java_type_reference(_, "IMPLEMENTS_INTERFACE", sub, _, _, // the ones that read the type regardless (lookup, dispatch below) see the node. field_type_external(field, x) :- java_type_reference(_, "FIELD_TYPE", _, _, _, _, _, "0", _, _, _, _, _, _, _, field, "FIELD", ref), type_ref_external(ref, x). +// A CATCH parameter carries no LOCAL_VARIABLE type reference (the parser records its type on the +// local only), so `catch (IOException e) { throw new Failure(e); }` left `e` untyped and both +// Failure(String) and Failure(Throwable) stayed candidates. Its declared type names the external +// node exactly as a reference would: the single-type import of the name, else the name as written. +// A multi-catch (`A | B e`) has no one type and is left alone. +.decl catch_local_type(local:symbol, tn:symbol, fp:symbol) +catch_local_type(local, tn, fp) :- java_local_variable(_, tn, _, _, _, fp, _, _, "CATCH_CLAUSE", _, _, _, _, _, _, _, _, _, local), + tn != "", !contains("|", tn), !contains("<", tn), + !name_resolves_in_file(tn, fp), + !simple_name_declared(tn). +local_var_type_external(local, x) :- catch_local_type(local, tn, fp), + java_import("SINGLE_TYPE", q, _, tn, fp, _, _, _, _, _, _), + !qualified_name_declared(q), + x = cat("external:", q). +local_var_type_external(local, x) :- catch_local_type(local, tn, fp), + !type_has_explicit_import(tn, fp), + x = cat("external:", tn). +external_type(x) :- local_var_type_external(_, x). local_var_type_external(local, x) :- java_type_reference(_, "LOCAL_VARIABLE", _, _, _, _, _, "0", _, _, _, _, _, _, _, local, "LOCAL_VARIABLE", ref), type_ref_external(ref, x). method_param_type_external(param, x) :- java_type_reference(_, "METHOD_PARAM", _, _, _, _, _, "0", _, _, _, _, _, _, _, param, "METHOD_PARAM", ref), diff --git a/graph/java/engine/resolution/param-flow.dl b/graph/java/engine/resolution/param-flow.dl index 1e03b9cb6..9d9c65041 100644 --- a/graph/java/engine/resolution/param-flow.dl +++ b/graph/java/engine/resolution/param-flow.dl @@ -30,11 +30,39 @@ // concrete type flowing into callee's parameter at position `pos` (from a candidate call's // ARGUMENT at that position). -param_flow_type(param, t) :- +// +// ONLY A TYPE THE PARAMETER CAN HOLD. The candidates at a site are every overload of the name, so +// an argument's type also flowed into the overloads it cannot be passed to: in +// `fromJson(JsonElement json, Type t) { return fromJson(json, TypeToken.get(t)); }` the String, +// Reader and JsonElement arguments of the sibling overloads' recursive calls all flowed into each +// other's `json`, every overload's parameter then held all of them, and the overload choice -- which +// reads exactly that type -- found a contradiction at every candidate and kept them all. A flowed +// type is kept where it is the declared type, a subtype of it, or the declared type is Object or +// unresolved (a type variable, an unstaged type with no external node): narrowing, never widening. +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves(_, param, _, d), t = d. +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves(_, param, _, d), type_ancestor(t, d). +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves(_, param, _, d), param_is_object(d). +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), t = x. +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), type_ancestor(t, x). +param_flow_type(param, t) :- param_flow_in(param, t), param_decl_untyped(param). +// A type whose hierarchy leaves the project (an exception class over an unstaged IOException) may +// reach an unstaged or library declared type through supertypes no IR shows, so it is kept against +// one; never against String, which is final. +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), + type_has_external_ancestor(t), !string_type_id(x). +param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves(_, param, _, d), + type_prov(d, "lib"), type_has_external_ancestor(t), !string_type_id(d). +.decl param_flow_in(param:symbol, t:symbol) +param_flow_in(param, t) :- applicable_candidate(call, callee), java_expression(_, "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, argE), expr_type(_, argE, t), java_method_parameter(_, pos, callee, _, _, _, _, _, _, _, _, _, param). +.decl param_decl_typed(param:symbol) +param_decl_typed(param) :- method_param_type_resolves(_, param, _, _). +param_decl_typed(param) :- method_param_type_external(param, _). +.decl param_decl_untyped(param:symbol) +param_decl_untyped(param) :- java_method_parameter(_, _, _, _, _, _, _, _, _, _, _, _, param), !param_decl_typed(param). // PARAMETER receiver reference takes the flow-in type(s) (positive; cap drops residual fan). expr_type("client", recv, t) :- diff --git a/graph/test/java/expected/29-qualified-this.edges b/graph/test/java/expected/29-qualified-this.edges index 57a8a4cf6..df51491cf 100644 --- a/graph/test/java/expected/29-qualified-this.edges +++ b/graph/test/java/expected/29-qualified-this.edges @@ -15,6 +15,7 @@ known_edge method probe.QualifiedThis$anon:Runnable#run() -> probe.QualifiedThis known_edge method probe.QualifiedThis.Inner#viaPlainThis() -> probe.QualifiedThis.Inner#tag() known_edge method probe.QualifiedThis.Inner#viaQualifiedThis() -> probe.QualifiedThis#tag() known_edge method probe.QualifiedThis.Inner#viaQualifiedThisNoClash() -> probe.QualifiedThis#only() +known_edge method probe.QualifiedThis.Inner#viaUnqualified() -> probe.QualifiedThis.Inner#tag() known_edge method probe.QualifiedThis.Inner.Deeper#oneLevelOut() -> probe.QualifiedThis.Inner#tag() known_edge method probe.QualifiedThis.Inner.Deeper#ownTag() -> probe.QualifiedThis.Inner.Deeper#tag() known_edge method probe.QualifiedThis.Inner.Deeper#twoLevelsOut() -> probe.QualifiedThis#tag() @@ -22,5 +23,3 @@ known_edge new probe.QualifiedThis#main(String[]) -> probe.QualifiedThis#( known_edge new probe.QualifiedThis#main(String[]) -> probe.QualifiedThis.Inner#() known_edge new probe.QualifiedThis#main(String[]) -> probe.QualifiedThis.Inner.Deeper#() multi_inferred method probe.QualifiedThis#main(String[]) -> probe.QualifiedThis$anon:Runnable#run() -multi_inferred method probe.QualifiedThis.Inner#viaUnqualified() -> probe.QualifiedThis#tag() -multi_inferred method probe.QualifiedThis.Inner#viaUnqualified() -> probe.QualifiedThis.Inner#tag() diff --git a/graph/test/java/expected/29-qualified-this.oracle b/graph/test/java/expected/29-qualified-this.oracle index a77fdcb74..6a9c9d9e4 100644 --- a/graph/test/java/expected/29-qualified-this.oracle +++ b/graph/test/java/expected/29-qualified-this.oracle @@ -1,4 +1,3 @@ -oracle=22 engine=23 agree=21 missing=1 (known 1, NEW 0) extra=2 +oracle=22 engine=22 agree=21 missing=1 (known 1, NEW 0) extra=1 known-missing probe.QualifiedThis#anon -> probe.QualifiedThis$anon:Runnable# extra probe.QualifiedThis#main -> probe.QualifiedThis$anon:Runnable#run() - extra probe.QualifiedThis.Inner#viaUnqualified -> probe.QualifiedThis#tag() diff --git a/tests/cases/java/overload-argument-types-without-the-platform/case.json b/tests/cases/java/overload-argument-types-without-the-platform/case.json new file mode 100644 index 000000000..20b34fc0c --- /dev/null +++ b/tests/cases/java/overload-argument-types-without-the-platform/case.json @@ -0,0 +1,18 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "a string concatenation is a String without the JDK staged", + "run": ["impact", "User.concat", "--depends"], "want": ["Sink.java:7"], "avoid": ["one of a set"]}, + {"why": "a class literal is a Class: read(Class) and not read(Node) or read(String)", + "run": ["impact", "User.literal", "--depends"], "want": ["Sink.java:10"], "avoid": ["one of a set"]}, + {"why": "a creation of an unstaged type is that type: load(Reader) is not load(String)", + "run": ["impact", "User.creation", "--depends"], "want": ["Sink.java:14"], "avoid": ["one of a set"]}, + {"why": "a catch parameter has its declared type: never a String", + "run": ["impact", "User.caught", "--depends"], "want": ["Sink.java:16"], "avoid": ["Sink.java:15"]}, + {"why": "String.class is a Class, not a Type it merely implements", + "run": ["impact", "User.classLit", "--depends"], "want": ["Sink.java:18"], "avoid": ["one of a set"]}, + {"why": "a primitive cast names the value's type", + "run": ["impact", "User.castChar", "--depends"], "want": ["Sink.java:19"], "avoid": ["one of a set"]}, + {"why": "a client-typed variable is never a String: String is final", + "run": ["impact", "User.nodeArg", "--depends"], "want": ["Sink.java:23"], "avoid": ["one of a set"]}, + {"why": "control: an argument that is a call's result keeps both overloads", + "run": ["impact", "User.callArg", "--depends"], "want": ["one of a set", "Sink.java:22", "Sink.java:23"]}]} diff --git a/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Node.java b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Node.java new file mode 100644 index 000000000..c7ee44f62 --- /dev/null +++ b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Node.java @@ -0,0 +1,3 @@ +package pkg; + +public class Node { } diff --git a/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Sink.java b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Sink.java new file mode 100644 index 000000000..d739390c0 --- /dev/null +++ b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/Sink.java @@ -0,0 +1,24 @@ +package pkg; + +import java.io.Reader; +import java.lang.reflect.Type; + +public class Sink { + void log(String s) { } + void log(Node n) { } + void log(int i) { } + void read(Class c) { } + void read(Node n) { } + void read(String s) { } + void load(String s) { } + void load(Reader r) { } + void wrap(String s) { } + void wrap(Throwable t) { } + void kind(Type t) { } + void kind(Class c) { } + void add(char c) { } + void add(String s) { } + void add(Node n) { } + void take(String s) { } + void take(Node n) { } +} diff --git a/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/User.java b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/User.java new file mode 100644 index 000000000..e48308356 --- /dev/null +++ b/tests/cases/java/overload-argument-types-without-the-platform/src/pkg/User.java @@ -0,0 +1,16 @@ +package pkg; + +import java.io.StringReader; + +// no JDK staged: every argument below is typed by the expression itself +public class User { + void concat(Sink s, int n) { s.log("total " + n); } + void literal(Sink s) { s.read(Node.class); } + void creation(Sink s) { s.load(new StringReader("x")); } + void caught(Sink s) { try { s.load("x"); } catch (RuntimeException e) { s.wrap(e); } } + void classLit(Sink s) { s.kind(String.class); } + void castChar(Sink s, int c) { s.add((char) c); } + void nodeArg(Sink s, Node n) { s.take(n); } + // control: a call's result is not read for its type, so both take overloads stay + void callArg(Sink s, StringBuilder b) { s.take(b.toString()); } +} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json b/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json new file mode 100644 index 000000000..3db07c7cd --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json @@ -0,0 +1,14 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "a JsonElement-like parameter passed on keeps its own type: the String and Reader arguments of the sibling overloads' calls never flow into it", + "run": ["impact", "src/pkg/Codec.java:12", "--depends"], + "want": ["Codec.java:13"], + "avoid": ["one of a set"]}, + {"why": "and the String overload picks its String sibling", + "run": ["impact", "src/pkg/Codec.java:9", "--depends"], + "want": ["Codec.java:10"], + "avoid": ["one of a set"]}, + {"why": "control: a subtype argument still reaches the overload whose parameter it fits", + "run": ["impact", "src/pkg/Codec.java:16", "--depends"], + "want": ["Codec.java:13"], + "avoid": ["one of a set"]}]} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Codec.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Codec.java new file mode 100644 index 000000000..e45516583 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Codec.java @@ -0,0 +1,17 @@ +package pkg; + +import java.io.Reader; +import java.io.StringReader; +import java.lang.reflect.Type; + +// overloads that call each other: each one's argument used to flow into every sibling's `json` +public class Codec { + public T from(String json, Type t) { return from(json, Tok.get(t)); } + public T from(String json, Tok t) { return from(new StringReader(json), t); } + public T from(Reader json, Tok t) { return from(Parser.parse(json), t); } + public T from(El json, Type t) { return from(json, Tok.get(t)); } + public T from(El json, Tok t) { return null; } + + // control: a subtype argument still flows into the parameter it fits + public T leaf(Leaf json, Type t) { return from(json, Tok.get(t)); } +} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/El.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/El.java new file mode 100644 index 000000000..f45724554 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/El.java @@ -0,0 +1,3 @@ +package pkg; + +public abstract class El { } diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Leaf.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Leaf.java new file mode 100644 index 000000000..6537070d7 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Leaf.java @@ -0,0 +1,3 @@ +package pkg; + +public class Leaf extends El { } diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Parser.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Parser.java new file mode 100644 index 000000000..ced730880 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Parser.java @@ -0,0 +1,7 @@ +package pkg; + +import java.io.Reader; + +public class Parser { + static El parse(Reader r) { return new Leaf(); } +} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Tok.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Tok.java new file mode 100644 index 000000000..b37f1353d --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Tok.java @@ -0,0 +1,8 @@ +package pkg; + +import java.lang.reflect.Type; + +public class Tok { + public static Tok get(Type t) { return null; } + public static Tok get(Class c) { return null; } +} diff --git a/tests/cases/java/unqualified-call-stops-at-the-innermost-member/case.json b/tests/cases/java/unqualified-call-stops-at-the-innermost-member/case.json new file mode 100644 index 000000000..bfb044a60 --- /dev/null +++ b/tests/cases/java/unqualified-call-stops-at-the-innermost-member/case.json @@ -0,0 +1,8 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "an unqualified call in a nested class is the innermost class's own method of that name (JLS 15.12.1), never the outer one it shadows", + "run": ["impact", "src/pkg/Doc.java:9", "--depends"], "want": ["Doc.java:8"], "avoid": ["Doc.java:4", "one of a set"]}, + {"why": "an inherited member shadows the outer one as well", + "run": ["impact", "src/pkg/Doc.java:20", "--depends"], "want": ["Doc.java:18"], "avoid": ["Doc.java:5"]}, + {"why": "control: a name only the outer class declares is still found there", + "run": ["impact", "src/pkg/Doc.java:14", "--depends"], "want": ["Doc.java:5"]}]} diff --git a/tests/cases/java/unqualified-call-stops-at-the-innermost-member/src/pkg/Doc.java b/tests/cases/java/unqualified-call-stops-at-the-innermost-member/src/pkg/Doc.java new file mode 100644 index 000000000..ba44f8f9c --- /dev/null +++ b/tests/cases/java/unqualified-call-stops-at-the-innermost-member/src/pkg/Doc.java @@ -0,0 +1,22 @@ +package pkg; + +public class Doc { + public void charset(String c) { } + public void reset() { } + + public static class Settings { + public Settings charset(String c) { return this; } + public Settings() { charset("utf-8"); } + } + + // control: a name the inner class does not have is still the outer one's + public class Inner { + void go() { reset(); } + } + + // a member inherited by the inner class shadows the outer one too + public static class Base { void reset() { } } + public static class Child extends Base { + void go() { reset(); } + } +} From 778e8b4ce9bbe735cb5022a5d5f5d21bd541dd74 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:39:08 -0700 Subject: [PATCH 2/3] fix(java): a handed-over object is called back only through library overrides; param flow past an unstaged type needs an unstaged superclass - callback_dispatch: rule (b) took every @Override method of a type with an external ancestor; an override of a CLIENT declaration that is itself no override (JsonArray.deepCopy over JsonElement's abstract deepCopy) is not something a library holding the object can call. - param-flow: a client type is kept against an unstaged / library parameter type only when it EXTENDS an unstaged class (a stream over FilterInputStream, an exception over IOException); implementing an unstaged interface is not enough (a CharSequence builder is no StringBuffer). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../call-edge-generation/callback_dispatch.dl | 10 ++- graph/java/engine/resolution/param-flow.dl | 15 ++-- .../case.json | 6 ++ .../src/pkg/Arr.java | 15 ++++ .../src/pkg/Elem.java | 5 ++ .../src/pkg/Writer.java | 14 ++++ .../case.json | 80 ++++++++++++++++--- .../src/pkg/Counted.java | 16 ++++ .../src/pkg/Sb.java | 13 +++ 9 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 tests/cases/java/handed-over-object-runs-only-library-overrides/case.json create mode 100644 tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Arr.java create mode 100644 tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Elem.java create mode 100644 tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Writer.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Counted.java create mode 100644 tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Sb.java diff --git a/graph/java/engine/call-edge-generation/callback_dispatch.dl b/graph/java/engine/call-edge-generation/callback_dispatch.dl index 27a2abb7f..29770ce32 100644 --- a/graph/java/engine/call-edge-generation/callback_dispatch.dl +++ b/graph/java/engine/call-edge-generation/callback_dispatch.dl @@ -33,6 +33,8 @@ // `defines` hop: it is lexically inside the method that hands it over. // ============================================================================ +.decl cb_client_override(m:symbol) +.decl cb_lib_overridden(b:symbol) cb_object_member("equals"). cb_object_member("hashCode"). cb_object_member("toString"). cb_object_member("clone"). cb_object_member("finalize"). @@ -69,7 +71,13 @@ cb_callback(t, m) :- cb_member(t, name, m), !cb_object_member(name), // (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, _). + ann_on_method("client", _, "Override", m, _), !cb_client_override(m). +// ...unless the @Override is explained by a CLIENT declaration that is itself no override: `deepCopy()` +// in `class JsonArray extends JsonElement implements Iterable` overrides JsonElement's own +// abstract deepCopy, which no library declares, so handing a JsonArray to a list cannot run it. +cb_client_override(m) :- virtual_override(base, m), method_owner("client", _, base), + !ann_on_method("client", _, "Override", base, _), !cb_lib_overridden(base). +cb_lib_overridden(b) :- virtual_override(lb, b), method_owner("lib", _, lb). // (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 diff --git a/graph/java/engine/resolution/param-flow.dl b/graph/java/engine/resolution/param-flow.dl index 9d9c65041..71e9c2005 100644 --- a/graph/java/engine/resolution/param-flow.dl +++ b/graph/java/engine/resolution/param-flow.dl @@ -45,13 +45,18 @@ param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), t = x. param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), type_ancestor(t, x). param_flow_type(param, t) :- param_flow_in(param, t), param_decl_untyped(param). -// A type whose hierarchy leaves the project (an exception class over an unstaged IOException) may -// reach an unstaged or library declared type through supertypes no IR shows, so it is kept against -// one; never against String, which is final. +// A class EXTENDING an unstaged class (an exception over an unstaged IOException, a stream over an +// unstaged FilterInputStream) may reach an unstaged or library declared type through superclasses no +// IR shows, so it is kept against one; never against String, which is final. Implementing an +// unstaged interface is not enough: a builder that is a CharSequence is no StringBuffer. +.decl param_flow_ext_super(t:symbol) +param_flow_ext_super(t) :- java_type_reference(_, "SUPER_TYPE", t, _, _, _, _, "0", _, _, _, _, _, _, _, _, _, ref), + type_ref_external(ref, _). +param_flow_ext_super(t) :- type_ancestor(t, a), param_flow_ext_super(a). param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_external(param, x), - type_has_external_ancestor(t), !string_type_id(x). + param_flow_ext_super(t), !string_type_id(x). param_flow_type(param, t) :- param_flow_in(param, t), method_param_type_resolves(_, param, _, d), - type_prov(d, "lib"), type_has_external_ancestor(t), !string_type_id(d). + type_prov(d, "lib"), param_flow_ext_super(t), !string_type_id(d). .decl param_flow_in(param:symbol, t:symbol) param_flow_in(param, t) :- applicable_candidate(call, callee), diff --git a/tests/cases/java/handed-over-object-runs-only-library-overrides/case.json b/tests/cases/java/handed-over-object-runs-only-library-overrides/case.json new file mode 100644 index 000000000..a42963dc7 --- /dev/null +++ b/tests/cases/java/handed-over-object-runs-only-library-overrides/case.json @@ -0,0 +1,6 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "an object handed to a library is called back only through what it overrides from a library type: copy() overrides a client declaration", + "run": ["impact", "Arr.copy"], "avoid": ["Writer.begin"]}, + {"why": "control: iterator() overrides the library's Iterable, so the hand-over still reaches it", + "run": ["impact", "Arr.iterator"], "want": ["Writer.begin"]}]} diff --git a/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Arr.java b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Arr.java new file mode 100644 index 000000000..9829c6058 --- /dev/null +++ b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Arr.java @@ -0,0 +1,15 @@ +package pkg; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Arr extends Elem implements Iterable { + private final List items = new ArrayList<>(); + + @Override + public Elem copy() { return new Arr(); } + + @Override + public Iterator iterator() { return items.iterator(); } +} diff --git a/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Elem.java b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Elem.java new file mode 100644 index 000000000..16b733981 --- /dev/null +++ b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Elem.java @@ -0,0 +1,5 @@ +package pkg; + +public abstract class Elem { + public abstract Elem copy(); +} diff --git a/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Writer.java b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Writer.java new file mode 100644 index 000000000..467ea80ac --- /dev/null +++ b/tests/cases/java/handed-over-object-runs-only-library-overrides/src/pkg/Writer.java @@ -0,0 +1,14 @@ +package pkg; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class Writer { + private final Deque stack = new ArrayDeque<>(); + + // the deque is handed an Arr: it may call what Arr overrides from a library type, never copy() + public void begin() { + Arr array = new Arr(); + stack.push(array); + } +} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json b/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json index 3db07c7cd..a3c41c3f8 100644 --- a/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/case.json @@ -1,14 +1,68 @@ -{"lang": "java", "src": "src", +{ + "lang": "java", + "src": "src", "checks": [ - {"why": "a JsonElement-like parameter passed on keeps its own type: the String and Reader arguments of the sibling overloads' calls never flow into it", - "run": ["impact", "src/pkg/Codec.java:12", "--depends"], - "want": ["Codec.java:13"], - "avoid": ["one of a set"]}, - {"why": "and the String overload picks its String sibling", - "run": ["impact", "src/pkg/Codec.java:9", "--depends"], - "want": ["Codec.java:10"], - "avoid": ["one of a set"]}, - {"why": "control: a subtype argument still reaches the overload whose parameter it fits", - "run": ["impact", "src/pkg/Codec.java:16", "--depends"], - "want": ["Codec.java:13"], - "avoid": ["one of a set"]}]} + { + "why": "a JsonElement-like parameter passed on keeps its own type: the String and Reader arguments of the sibling overloads' calls never flow into it", + "run": [ + "impact", + "src/pkg/Codec.java:12", + "--depends" + ], + "want": [ + "Codec.java:13" + ], + "avoid": [ + "one of a set" + ] + }, + { + "why": "and the String overload picks its String sibling", + "run": [ + "impact", + "src/pkg/Codec.java:9", + "--depends" + ], + "want": [ + "Codec.java:10" + ], + "avoid": [ + "one of a set" + ] + }, + { + "why": "control: a subtype argument still reaches the overload whose parameter it fits", + "run": [ + "impact", + "src/pkg/Codec.java:16", + "--depends" + ], + "want": [ + "Codec.java:13" + ], + "avoid": [ + "one of a set" + ] + }, + { + "why": "a client type that only implements an unstaged interface never flows into a StringBuffer parameter: StringBuffer's getChars is not Sb's", + "run": [ + "impact", + "Sb.getChars" + ], + "avoid": [ + "Sb.java:10 — calls" + ] + }, + { + "why": "control: a class extending an unstaged class still flows into the unstaged parameter it may be", + "run": [ + "impact", + "Counted.read" + ], + "want": [ + "Counted.first" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Counted.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Counted.java new file mode 100644 index 000000000..66a9fdf86 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Counted.java @@ -0,0 +1,16 @@ +package pkg; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +// extends an unstaged class: may well be the InputStream a parameter declares +public class Counted extends FilterInputStream { + public Counted(InputStream in) { super(in); } + + @Override + public int read() throws IOException { return super.read(); } + + static int first(InputStream in) throws IOException { return in.read(); } + static int open(InputStream raw) throws IOException { return first(new Counted(raw)); } +} diff --git a/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Sb.java b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Sb.java new file mode 100644 index 000000000..968e503c2 --- /dev/null +++ b/tests/cases/java/param-flow-only-what-the-parameter-holds/src/pkg/Sb.java @@ -0,0 +1,13 @@ +package pkg; + +// implements an unstaged interface, extends nothing unstaged: never a StringBuffer +public class Sb implements CharSequence { + public int length() { return 0; } + public char charAt(int i) { return 0; } + public CharSequence subSequence(int a, int b) { return this; } + public void getChars(int a, int b, char[] dst, int at) { } + + public Sb add(StringBuffer s) { s.getChars(0, s.length(), new char[0], 0); return this; } + public Sb add(Sb other) { other.getChars(0, 0, new char[0], 0); return this; } + public Sb use(Sb x) { return add(x); } +} From 5c6a14055404a9a122ea4350e18e644affac5ad3 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:08:30 -0700 Subject: [PATCH 3/3] fix(java): the dispatch cap is read at the receiver's type; an abstract base is no way in from a site that lists its overrides - dispatch-cap / callee-resolution / call_chain: a base wide over its whole hierarchy was refused at every site, even one whose receiver type has two overrides under it (`StrLookup.noneLookup().lookup(k)` naming StringLookup#lookup). The width is counted under the receiver's type (raw hierarchy, so the negation still stratifies); a site is fan_capped only where one of its receiver types is wide. - impact.dl: a caller whose every site to an ABSTRACT base lists the overrides it may run is reached from those overrides, not through the bodiless base (abstract_m facts); and a base the target overrides is no way in from a fanned site that lacks the target (IMPACT_VERSION 77). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/call_chain.dl | 13 ++++- .../callee-resolution.dl | 4 +- graph/java/engine/resolution/dispatch-cap.dl | 29 ++++++++++ .../skills/axiomcode/scripts/axiomcode-impact | 12 +++- .../skills/axiomcode/scripts/dl/impact.dl | 27 ++++++++- .../case.json | 56 +++++++++++++++++++ .../src/pkg/Lookup.java | 5 ++ .../src/pkg/Mark.java | 5 ++ .../src/pkg/Narrow.java | 10 ++++ .../src/pkg/Plain.java | 29 ++++++++++ 10 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tests/cases/java/dispatch-cap-read-at-the-receiver-type/case.json create mode 100644 tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Lookup.java create mode 100644 tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Mark.java create mode 100644 tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Narrow.java create mode 100644 tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Plain.java diff --git a/graph/java/engine/call-edge-generation/call_chain.dl b/graph/java/engine/call-edge-generation/call_chain.dl index 2e904be31..3ec3d3434 100644 --- a/graph/java/engine/call-edge-generation/call_chain.dl +++ b/graph/java/engine/call-edge-generation/call_chain.dl @@ -99,7 +99,18 @@ site_leaves_client(call) :- external_target(call, _). // dispatch_capped_site carries the count. The conditions are exactly the ones under which the // fan would have applied: not an exactly typed receiver, not a super call, a wide base. call_fan_capped(call) :- invocation_site(call, _), expr_call_candidate(call, base), cha_wide_base(base), - !call_exact_receiver(call), !call_super_receiver(call). + !call_exact_receiver(call), !call_super_receiver(call), call_recv_wide(call, base). +// ...where the fan was refused at one of the receiver's own types (resolution/dispatch-cap.dl): a receiver +// typed only by narrow types took its fan under them, and is no capped site +.decl call_recv_wide(call:symbol, base:symbol) +.decl call_recv_typed(call:symbol) +call_recv_typed(call) :- call_site(call, _, recv), expr_type(_, recv, _). +call_recv_typed(call) :- call_unqualified(call, _), expr_ultimate_type("client", call, _). +call_recv_wide(call, base) :- expr_call_candidate(call, base), cha_wide_base(base), + call_site(call, _, recv), expr_type(_, recv, rt), cha_wide_for(base, rt). +call_recv_wide(call, base) :- expr_call_candidate(call, base), cha_wide_base(base), + call_unqualified(call, _), expr_ultimate_type("client", call, et), cha_wide_for(base, et). +call_recv_wide(call, base) :- expr_call_candidate(call, base), cha_wide_base(base), !call_recv_typed(call). dispatch_capped_site(call, base, n) :- call_fan_capped(call), expr_call_candidate(call, base), cha_wide_base(base), cha_override_count(base, n). call_class(call, "fan_capped") :- call_fan_capped(call). diff --git a/graph/java/engine/expression-resolution/callee-resolution.dl b/graph/java/engine/expression-resolution/callee-resolution.dl index c101400c1..f679ea7cf 100644 --- a/graph/java/engine/expression-resolution/callee-resolution.dl +++ b/graph/java/engine/expression-resolution/callee-resolution.dl @@ -175,17 +175,17 @@ expr_call_candidate(call, override) :- expr_call_candidate(call, base), virtual_override(base, override), !call_exact_receiver(call), !call_super_receiver(call), - !cha_wide_base(base), call_site(call, _, recv), expr_type(_, recv, rt), + !cha_wide_for(base, rt), override_in_closure(override, rt). // receiverless call: the receiver is `this`, so gate on the ENCLOSING type's closure. expr_call_candidate(call, override) :- expr_call_candidate(call, base), virtual_override(base, override), !call_exact_receiver(call), !call_super_receiver(call), - !cha_wide_base(base), call_unqualified(call, _), expr_ultimate_type("client", call, et), + !cha_wide_for(base, et), override_in_closure(override, et). // ── ctor_of(Prov, OwnerType, Ctor) — a constructor a `new` can target ──────────────────── diff --git a/graph/java/engine/resolution/dispatch-cap.dl b/graph/java/engine/resolution/dispatch-cap.dl index 2d3e8ffb2..b02b54f9a 100644 --- a/graph/java/engine/resolution/dispatch-cap.dl +++ b/graph/java/engine/resolution/dispatch-cap.dl @@ -63,3 +63,32 @@ cha_override_count(base, n) :- cha_override_low(base, _), cha_wide_base(base) :- dispatch_cap(k), cha_override_count(base, n), n > to_number(k). + +// ── THE CAP IS READ AT THE RECEIVER'S TYPE ────────────────────────────────────────────────── +// A base is wide over its whole hierarchy, but a call is made on a receiver of some type R, and the +// fan the call can take is only the overrides reachable under R. `StrLookup.noneLookup().lookup(k)` +// names StringLookup#lookup (twenty-odd implementations) on a StrLookup (two): refusing the fan for +// the base's width left the declaration as the site's one candidate, and a reverse walk from ANY +// StringLookup implementation then found that site through the declaration. The width is now +// counted under R (an override declared in R or a subtype of R), from raw hierarchy, so it stays +// LOW like the base count and its negation still stratifies; the base count is unchanged for a +// receiver whose type is the base's own owner. +.decl cha_override_under(base:symbol, rt:symbol, ov:symbol) +cha_override_under(base, rt, ov) :- cha_override_low(base, ov), + java_method(_, _, _, _, _, _, _, rt, _, _, _, _, _, _, _, _, _, _, _, _, _, ov). +cha_override_under(base, rt, ov) :- cha_override_low(base, ov), + java_method(_, _, _, _, _, _, _, o, _, _, _, _, _, _, _, _, _, _, _, _, _, ov), + type_ancestor(o, rt). +.decl cha_under_count(base:symbol, rt:symbol, n:number) +cha_under_count(base, rt, n) :- cha_override_under(base, rt, _), + n = count : { cha_override_under(base, rt, _) }. +.decl cha_wide_for(base:symbol, rt:symbol) +cha_wide_for(base, rt) :- dispatch_cap(k), cha_wide_base(base), cha_under_count(base, rt, n), n > to_number(k). +// a receiver type no counted override sits under is narrow; one that is the base's owner (or above +// it) sees the whole fan, so it is wide exactly when the base is +cha_wide_for(base, rt) :- cha_wide_base(base), + java_method(_, _, _, _, _, _, _, bt, _, _, _, _, _, _, _, _, _, _, _, _, _, base), rt = bt. +cha_wide_for(base, rt) :- cha_wide_base(base), + java_method(_, _, _, _, _, _, _, bt, _, _, _, _, _, _, _, _, _, _, _, _, _, base), type_ancestor(bt, rt). +cha_wide_for(base, rt) :- cha_wide_base(base), + lib_method(_, _, _, _, _, _, _, bt, _, _, _, _, _, _, _, _, _, _, _, _, _, base), rt = bt. diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 121c6716a..e84abc787 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -822,6 +822,14 @@ class Impact: out |= {(f['id'],) for f in self.fields.values() if f.get('id') in static_f} return sorted(out) + def java_abstract_methods(self): + """(m,) for each Java method declared without a body (an interface or abstract method): nothing runs it, a call + typed to it runs one of its overrides.""" + g = self.g + if not g.has('methods'): return [] + ab = {r['id'] for r in g.q("SELECT id FROM methods WHERE kind = 'ABSTRACT_METHOD' AND file_path LIKE '%.java'")} + return sorted({(i,) for i, s in g.sym.items() if s.get('method_id') in ab}) + def java_init_calls(self): """(t, b, static|instance): a Java type t calls b from a field initializer (the engine names the TYPE as the caller of a call written outside every method). A static field's or enum constant's initializer runs once, when the type is @@ -1663,7 +1671,7 @@ class Impact: W('cs_fixture_type', sorted(x for x in fixt if not x[0].startswith('collection:'))) # ── facts: the graph, exported once (reused while graph.sqlite is unchanged) ──────────────────────────────── - IMPACT_VERSION = '76' # 76: (71 on its branch) walk_edge, no `defines` hop out of a Java initializer (a member written in it is not run by it); 75: (70 on its branch) jstatic / init_call, the `at load` hop taken from what initializes a Java type (JLS 12.4.1: new, a static member) and, for an instance initializer, to the type's constructors; 74: (70 on its branch) an unresolved bare call whose name the file imports from a third-party package is of the library kind (Impact.external_bare_sites); imports_fact reads a JavaScript / TypeScript import over several lines, side-effect / dynamic / mock imports, import.meta.glob, tsconfig paths, `#` imports, a workspace package's source and the implicit JSX runtime, and imports_unknown names the files whose imports were not all read; 73: the TypeScript recall branch's 67-69 under one number — 69: calls carries the edge from a call that selected an overload signature to the overload set's implementation (graph_sql.overload_impl_edges); 68: runs_before carries a vitest / jest set-up file's module (and a global set-up's exported setup) for the tests it runs before (ax_runner_setup); 67: test_method takes an anonymous test whose registrar call opens on an earlier line (graph_sql.continued_registrar_arg); 72: (70 on its branch) rejected, the leads an asserted-links rejection (or a link that settles the site) takes out of the walks (ax_links.suppressed); 71: (70 on its branch) imports_fact resolves a JavaScript directory import (`require('..')`, a folder with a package.json); imports_dynamic, the files that load a module by a computed name, and load_run (dl), test code that reaches the change through calls alone; 70: fn_returns / fn_returns_param / decorated_call (a by-name decorator hop lands on the wrapper the decorator returns), a decoration string naming a parameter of the declaration is no registration key; 69: a library-callback site is a library receiver only where a hand-back from it is summarised (ax_edges.library_receiver_sites); 68: both 67s below, from two branches; 67: spawns_fact also links the module a `python -m pkg.mod` spawn runs; 67: libcb, a library summarised to call a client member back (Python library-callbacks.dl), walked as fw_edge "library callback"; 66: imports_fact names a module from its package root (a src layout) and has each test file import the conftest.py files above it; 65: guard_only also reads a conjunctive guard (`cond and __name__ == "__main__"`); 64: guard_only, a module body's call made only under `if __name__ == "__main__":`, which the import walk does not take; 63: imports_fact resolves an import written with its extension and a workspace package's specifier, and test_method takes no JavaScript / TypeScript helper by its name; 62: state_gate, state_gate_alloc, state_call_alloc, state_call_open, state_world, the callbacks one instance was given and the allocation each caller's receiver may be (JavaScript instance-state.dl); 61: filter_run, a servlet filter a test's context holds (added to HttpSecurity in a configuration the test loads, or a component) and the test methods that send a request through it; 57: a TypeScript object literal key is a ref of entity kind OBJECT_PROPERTY_KEY, kept past a bound access on its line; 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key + IMPACT_VERSION = '77' # 77: abstract_m and base_fanned_only, an abstract base is no way in from a caller whose every site to it lists the overrides it may run; base_site_skip, a base the target overrides is no way in from a site that lists its overrides without the target; 76: (71 on its branch) walk_edge, no `defines` hop out of a Java initializer (a member written in it is not run by it); 75: (70 on its branch) jstatic / init_call, the `at load` hop taken from what initializes a Java type (JLS 12.4.1: new, a static member) and, for an instance initializer, to the type's constructors; 74: (70 on its branch) an unresolved bare call whose name the file imports from a third-party package is of the library kind (Impact.external_bare_sites); imports_fact reads a JavaScript / TypeScript import over several lines, side-effect / dynamic / mock imports, import.meta.glob, tsconfig paths, `#` imports, a workspace package's source and the implicit JSX runtime, and imports_unknown names the files whose imports were not all read; 73: the TypeScript recall branch's 67-69 under one number — 69: calls carries the edge from a call that selected an overload signature to the overload set's implementation (graph_sql.overload_impl_edges); 68: runs_before carries a vitest / jest set-up file's module (and a global set-up's exported setup) for the tests it runs before (ax_runner_setup); 67: test_method takes an anonymous test whose registrar call opens on an earlier line (graph_sql.continued_registrar_arg); 72: (70 on its branch) rejected, the leads an asserted-links rejection (or a link that settles the site) takes out of the walks (ax_links.suppressed); 71: (70 on its branch) imports_fact resolves a JavaScript directory import (`require('..')`, a folder with a package.json); imports_dynamic, the files that load a module by a computed name, and load_run (dl), test code that reaches the change through calls alone; 70: fn_returns / fn_returns_param / decorated_call (a by-name decorator hop lands on the wrapper the decorator returns), a decoration string naming a parameter of the declaration is no registration key; 69: a library-callback site is a library receiver only where a hand-back from it is summarised (ax_edges.library_receiver_sites); 68: both 67s below, from two branches; 67: spawns_fact also links the module a `python -m pkg.mod` spawn runs; 67: libcb, a library summarised to call a client member back (Python library-callbacks.dl), walked as fw_edge "library callback"; 66: imports_fact names a module from its package root (a src layout) and has each test file import the conftest.py files above it; 65: guard_only also reads a conjunctive guard (`cond and __name__ == "__main__"`); 64: guard_only, a module body's call made only under `if __name__ == "__main__":`, which the import walk does not take; 63: imports_fact resolves an import written with its extension and a workspace package's specifier, and test_method takes no JavaScript / TypeScript helper by its name; 62: state_gate, state_gate_alloc, state_call_alloc, state_call_open, state_world, the callbacks one instance was given and the allocation each caller's receiver may be (JavaScript instance-state.dl); 61: filter_run, a servlet filter a test's context holds (added to HttpSecurity in a configuration the test loads, or a component) and the test methods that send a request through it; 57: a TypeScript object literal key is a ref of entity kind OBJECT_PROPERTY_KEY, kept past a bound access on its line; 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key # layer; 15: the registration facts (two 14s landed independently, which is exactly the collision this # guards); 16: regsite folded into ax_registration's reg_key_fact; 20: implements_pair (#1011); 17/18: the tagged-template test registrar # (it.each`…`) and its table span @@ -1788,7 +1796,7 @@ class Impact: FROM field_access fa LEFT JOIN paths p ON p.raw = fa.file_path WHERE fa.field_id IS NOT NULL AND fa.caller_id IS NOT NULL""")] if g.has('field_access') else [] W('faccess', fa) - W('jstatic', self.java_static_members()); W('init_call', self.java_init_calls()) + W('jstatic', self.java_static_members()); W('init_call', self.java_init_calls()); W('abstract_m', self.java_abstract_methods()) self.module_owners = modules_used # the type written on the field's own declaration line. Without it `holds` is file scoped: one field of type T # in a file makes every type declared in that file a holder of T diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index a7641525e..fa2152476 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -888,7 +888,32 @@ seed_byname(q, c) :- valueref(q, c, _, _), !seed(q, c). .decl up(q:symbol, m:symbol, d:number) up(q, m, 0) :- seed(q, m). up(q, c, 1) :- seed_byname(q, c). -up(q, a, d+1) :- up(q, b, d), walk_edge(a, b, _), !state_gate(a, b, _), d < 40. +up(q, a, d+1) :- up(q, b, d), walk_edge(a, b, _), !state_gate(a, b, _), !base_site_skip(q, a, b), !fanned_base_skip(q, a, b), d < 40. +// A BASE THE TARGET OVERRIDES IS NOT A WAY IN FROM A SITE THAT ENUMERATED ITS OVERRIDES. The base is a seed ("it +// overrides this"), so every caller of the base was read as a caller of the target. Where the engine fanned the site +// (it lists overrides of the base there, the fan taken under the receiver's type) and the target is not among them, +// the receiver cannot hold the target's type: `n.lookup(k)` on a Narrow runs Narrow's two implementations, never a +// third class's, though all of them implement the one interface method the site names. A site that lists no +// override (the fan refused as too wide, or a concrete base) is still a way in. +// ...and the same for an ABSTRACT base reached anywhere on the walk (a dispatch hop from one of its overrides): a +// caller whose every site calling it lists the overrides the receiver may hold is reached from those overrides +// directly, so the bodiless base adds only the overrides it does NOT list — `StrLookup.noneLookup().lookup(k)` names +// StringLookup#lookup and lists StrLookup's three implementations; reaching it from XmlDecoderStringLookup#lookup +// through the interface method was a route the receiver's type rules out. Not when the base is the change itself. +.decl abstract_m(m:symbol) .input abstract_m +.decl base_unfanned_site(c:symbol, b:symbol) +base_unfanned_site(c, b) :- abstract_m(b), calls(c, b, _, f, l), !base_site_fanned(c, b, f, l). +.decl base_fanned_only(c:symbol, b:symbol) +base_fanned_only(c, b) :- abstract_m(b), calls(c, b, _, _, _), !base_unfanned_site(c, b). +.decl fanned_base_skip(q:symbol, c:symbol, b:symbol) +fanned_base_skip(q, c, b) :- target(q, _, _, _), base_fanned_only(c, b), !target(q, "method", b, _). +.decl base_site_fanned(c:symbol, b:symbol, f:symbol, l:number) +base_site_fanned(c, b, f, l) :- calls(c, b, _, f, l), calls(c, o, _, f, l), override(b, o). +.decl base_site_ok(q:symbol, c:symbol, b:symbol) +base_site_ok(q, c, b) :- contract(q, b, "it overrides this"), calls(c, b, _, f, l), !base_site_fanned(c, b, f, l). +base_site_ok(q, c, b) :- contract(q, b, "it overrides this"), target(q, "method", m, _), calls(c, b, _, f, l), calls(c, m, _, f, l). +.decl base_site_skip(q:symbol, c:symbol, b:symbol) +base_site_skip(q, c, b) :- contract(q, b, "it overrides this"), calls(c, b, _, _, _), !base_site_ok(q, c, b). // A CALLABLE WRITTEN INSIDE A JAVA INITIALIZER IS NOT RUN BY IT. `static { ACCESS = new Access() { void m() {..} }; }` and // `static final Comparator BY_NAME = (a, b) -> ..` build an object; its method runs when someone calls it on that // object, which the call edges into it already say. The `defines` hop from the initializer (a , an , diff --git a/tests/cases/java/dispatch-cap-read-at-the-receiver-type/case.json b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/case.json new file mode 100644 index 000000000..994e96e3a --- /dev/null +++ b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/case.json @@ -0,0 +1,56 @@ +{ + "lang": "java", + "src": "src", + "checks": [ + { + "why": "a call on a Narrow receiver can only run Narrow's two implementations, however wide the Lookup interface it names is", + "run": [ + "impact", + "Plain.L5.lookup" + ], + "avoid": [ + "viaNarrow" + ] + }, + { + "why": "and it does run them: the fan under the receiver type is taken, not refused", + "run": [ + "impact", + "Narrow.A.lookup" + ], + "want": [ + "viaNarrow" + ] + }, + { + "why": "control: a call on the wide interface itself is still one of every implementation", + "run": [ + "impact", + "Plain.L5.lookup" + ], + "want": [ + "viaWide" + ] + }, + { + "why": "the same for a change reached through the override: a Narrow call site is no caller of what L5.lookup calls", + "run": [ + "impact", + "Mark.mark" + ], + "avoid": [ + "viaNarrow" + ] + }, + { + "why": "control: the wide call site still reaches it through the interface method", + "run": [ + "impact", + "Mark.mark" + ], + "want": [ + "viaWide" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Lookup.java b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Lookup.java new file mode 100644 index 000000000..c31dd7c15 --- /dev/null +++ b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Lookup.java @@ -0,0 +1,5 @@ +package pkg; + +public interface Lookup { + String lookup(String key); +} diff --git a/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Mark.java b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Mark.java new file mode 100644 index 000000000..81bf04b9a --- /dev/null +++ b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Mark.java @@ -0,0 +1,5 @@ +package pkg; + +public class Mark { + static String mark(String k) { return k; } +} diff --git a/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Narrow.java b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Narrow.java new file mode 100644 index 000000000..902f27c05 --- /dev/null +++ b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Narrow.java @@ -0,0 +1,10 @@ +package pkg; + +// a narrower base with two implementations of its own +public abstract class Narrow implements Lookup { + public static final class A extends Narrow { public String lookup(String k) { return "a"; } } + public static final class B extends Narrow { public String lookup(String k) { return "b"; } } + + static String viaNarrow(Narrow n) { return n.lookup("x"); } + static String viaWide(Lookup l) { return l.lookup("x"); } +} diff --git a/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Plain.java b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Plain.java new file mode 100644 index 000000000..60658f90a --- /dev/null +++ b/tests/cases/java/dispatch-cap-read-at-the-receiver-type/src/pkg/Plain.java @@ -0,0 +1,29 @@ +package pkg; + +// many implementations: a call on a Lookup is too wide to fan +public final class Plain { + public static final class L1 implements Lookup { public String lookup(String k) { return "1"; } } + public static final class L2 implements Lookup { public String lookup(String k) { return "2"; } } + public static final class L3 implements Lookup { public String lookup(String k) { return "3"; } } + public static final class L4 implements Lookup { public String lookup(String k) { return "4"; } } + public static final class L5 implements Lookup { public String lookup(String k) { return Mark.mark(k); } } + public static final class L6 implements Lookup { public String lookup(String k) { return "6"; } } + public static final class L7 implements Lookup { public String lookup(String k) { return "7"; } } + public static final class L8 implements Lookup { public String lookup(String k) { return "8"; } } + public static final class L9 implements Lookup { public String lookup(String k) { return "9"; } } + public static final class L10 implements Lookup { public String lookup(String k) { return "10"; } } + public static final class L11 implements Lookup { public String lookup(String k) { return "11"; } } + public static final class L12 implements Lookup { public String lookup(String k) { return "12"; } } + public static final class L13 implements Lookup { public String lookup(String k) { return "13"; } } + public static final class L14 implements Lookup { public String lookup(String k) { return "14"; } } + public static final class L15 implements Lookup { public String lookup(String k) { return "15"; } } + public static final class L16 implements Lookup { public String lookup(String k) { return "16"; } } + public static final class L17 implements Lookup { public String lookup(String k) { return "17"; } } + public static final class L18 implements Lookup { public String lookup(String k) { return "18"; } } + public static final class L19 implements Lookup { public String lookup(String k) { return "19"; } } + public static final class L20 implements Lookup { public String lookup(String k) { return "20"; } } + public static final class L21 implements Lookup { public String lookup(String k) { return "21"; } } + public static final class L22 implements Lookup { public String lookup(String k) { return "22"; } } + public static final class L23 implements Lookup { public String lookup(String k) { return "23"; } } + public static final class L24 implements Lookup { public String lookup(String k) { return "24"; } } +}