From 0dbe14cee6f6a575a6a12c17d075d2acb2d38169 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:19:26 -0700 Subject: [PATCH] fix(java): narrower `at load` hop, platform receivers as boundaries, operator arguments in overload choice - impact: a type's static initializer is reached from what initializes the type (JLS 12.4.1: new, a static method call, a static field use); an instance initializer from the type's constructors. No `defines` hop out of an initializer: a lambda or anonymous class written in it is not run by it. - engine: a pinned local of an unstaged platform type takes its declared type; a static call on an unstaged type name is a library boundary rather than an unresolved site. - overload resolution: a numeric/boolean operator expression argument rules out reference-typed overloads. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/expression-resolution/overload.dl | 35 ++++++++++ .../java/engine/resolution/external-types.dl | 33 ++++++++- .../test/java/expected/02-anonymous-sam.edges | 4 +- .../expected/03-overloads-and-receivers.edges | 4 +- .../expected/05-lambda-and-method-refs.edges | 1 + .../12-library-interface-override.edges | 1 + .../expected/13-type-qualified-statics.edges | 3 +- .../expected/14-lambda-param-callback.edges | 2 +- .../expected/20-reflection-blind-spot.edges | 2 + .../java/expected/33-compiler-lowering.edges | 5 +- .../expected/48-local-scope-shadowing.edges | 2 +- .../59-function-value-through-ternary.edges | 4 +- .../java/expected/61-var-local-type.edges | 1 + ...67-framework-registered-entry-points.edges | 2 +- .../expected/68-http-routes-and-sends.edges | 2 +- .../skills/axiomcode/scripts/axiomcode-impact | 45 +++++++++++- .../skills/axiomcode/scripts/dl/impact.dl | 56 +++++++++++---- .../case.json | 10 +++ .../src/pkg/Access.java | 5 ++ .../src/pkg/PeekTest.java | 8 +++ .../src/pkg/Promoter.java | 7 ++ .../src/pkg/PromoterTest.java | 8 +++ .../src/pkg/Reader.java | 16 +++++ .../src/pkg/Tables.java | 7 ++ .../src/pkg/Upper.java | 7 ++ .../overload-by-operator-argument/case.json | 18 +++++ .../src/pkg/Bag.java | 20 ++++++ .../src/pkg/Node.java | 3 + .../src/pkg/Sizer.java | 15 ++++ .../src/pkg/SizerTest.java | 18 +++++ .../platform-receiver-is-a-boundary/case.json | 18 +++++ .../src/pkg/Circle.java | 5 ++ .../src/pkg/Formatter.java | 13 ++++ .../src/pkg/FormatterTest.java | 11 +++ .../src/pkg/Painter.java | 12 ++++ .../src/pkg/PainterTest.java | 11 +++ .../src/pkg/Report.java | 13 ++++ .../src/pkg/ReportTest.java | 8 +++ .../src/pkg/Shape.java | 3 + .../src/pkg/Square.java | 5 ++ .../case.json | 68 +++++++++++++++++++ .../src/pkg/Catalog.java | 20 ++++++ .../src/pkg/CatalogTest.java | 8 +++ .../src/pkg/Holder.java | 9 +++ .../src/pkg/HolderTest.java | 8 +++ .../src/pkg/Names.java | 10 +++ .../src/pkg/RowTest.java | 8 +++ .../src/pkg/Seeds.java | 10 +++ 48 files changed, 557 insertions(+), 27 deletions(-) create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/case.json create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Access.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PeekTest.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Promoter.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PromoterTest.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Reader.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Tables.java create mode 100644 tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Upper.java create mode 100644 tests/cases/java/overload-by-operator-argument/case.json create mode 100644 tests/cases/java/overload-by-operator-argument/src/pkg/Bag.java create mode 100644 tests/cases/java/overload-by-operator-argument/src/pkg/Node.java create mode 100644 tests/cases/java/overload-by-operator-argument/src/pkg/Sizer.java create mode 100644 tests/cases/java/overload-by-operator-argument/src/pkg/SizerTest.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/case.json create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Circle.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Formatter.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/FormatterTest.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Painter.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/PainterTest.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Report.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/ReportTest.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Shape.java create mode 100644 tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Square.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/case.json create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Catalog.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/CatalogTest.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Holder.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/HolderTest.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Names.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/RowTest.java create mode 100644 tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Seeds.java diff --git a/graph/java/engine/expression-resolution/overload.dl b/graph/java/engine/expression-resolution/overload.dl index 0e9188390..e312a933b 100644 --- a/graph/java/engine/expression-resolution/overload.dl +++ b/graph/java/engine/expression-resolution/overload.dl @@ -299,6 +299,41 @@ arg_value_type(call, pos, vt) :- java_expression(_, "ARGUMENT", _, _, _, _, call arg_value_type(call, pos, vt) :- java_expression(_, "ARGUMENT", _, _, _, _, call, pos, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, arg), expr_ultimate_type("client", arg, encl), java_field(name, vt, _, _, _, _, _, _, encl, _, _, _, _, _), value_type_name(vt). +// An ARITHMETIC or COMPARISON expression argument: `new Elements(elements.size() - 1)`, `remove(--i)`, `put(k, a == b)`. +// Its operands may be untyped (a call into an unstaged library), but the operator fixes what kind of value it is: +// `-` `*` `/` `%` and the shifts make a number, a comparison or `&&` / `||` / `!` a boolean. `+` is left out (a String +// concatenation is a `+` too), and so are `&` `|` `^` (numeric or boolean by operand). "numeric" is a value type of its +// own because the width is not known: it is compatible with every numeric primitive and wrapper, never with String, +// boolean or a reference type. Without it the call kept `Elements(Element...)` beside `Elements(int)`, and every +// caller of the one was a caller of the other. +// java_expression (25): 1 kind 2 edgeRole 7 parentExpressionHash 8 position 14 operatorString +.decl numeric_op(o:symbol) +.decl numeric_unary_op(o:symbol) +.decl boolean_op(o:symbol) +.decl numeric_prim(p:symbol) +.decl prim_value(v:symbol) +numeric_op("-"). numeric_op("*"). numeric_op("/"). numeric_op("%"). numeric_op("<<"). numeric_op(">>"). numeric_op(">>>"). +numeric_unary_op("-"). numeric_unary_op("+"). numeric_unary_op("++"). numeric_unary_op("--"). numeric_unary_op("~"). +boolean_op("=="). boolean_op("!="). boolean_op("<"). boolean_op(">"). boolean_op("<="). boolean_op(">="). boolean_op("&&"). boolean_op("||"). +arg_value_type(call, pos, "numeric") :- java_expression("BINARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + numeric_op(op). +arg_value_type(call, pos, "numeric") :- java_expression("UNARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + numeric_unary_op(op). +arg_value_type(call, pos, "boolean") :- java_expression("BINARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, op, _, _, _, _, _, _, _, _, _, _, _), + boolean_op(op). +arg_value_type(call, pos, "boolean") :- java_expression("UNARY_EXPRESSION", "ARGUMENT", _, _, _, _, call, pos, _, _, _, _, _, "!", _, _, _, _, _, _, _, _, _, _, _). +numeric_prim("int"). numeric_prim("long"). numeric_prim("float"). numeric_prim("double"). +numeric_prim("short"). numeric_prim("byte"). numeric_prim("char"). +value_compatible("numeric", b) :- value_prim(b, p), numeric_prim(p). +// A primitive value (a literal, a primitive variable, an arithmetic or comparison expression) never reaches a +// parameter typed as a CLIENT type: boxing makes a java.lang wrapper, which no project type is. +prim_value(v) :- value_prim(v, v), v != "String", v != "null". +prim_value("numeric"). +value_mismatch(call, callee) :- expr_call_candidate(call, callee), + arg_value_type(call, pos, at), prim_value(at), + param_type_hash(callee, pos, pt), type_prov(pt, "client"), + !arg_is_target_typed(call, pos). + // Parameter value-type (parameterTypeName col 4 is the simple name: int / String / ...). param_value_type(callee, pos, vt) :- java_method_parameter(_, pos, callee, _, vt, _, _, _, _, _, _, _, _), value_type_name(vt). param_value_type(callee, pos, vt) :- lib_method_parameter(_, pos, callee, _, vt, _, _, _, _, _, _, _, _), value_type_name(vt). diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl index 4b83b6a19..46427be85 100644 --- a/graph/java/engine/resolution/external-types.dl +++ b/graph/java/engine/resolution/external-types.dl @@ -151,7 +151,24 @@ local_creation_init(local) :- java_expression("ANONYMOUS_CLASS_CREATION", "ROOT" local_reassigned(local) :- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local), java_expression(_, "ASSIGNMENT_TARGET", _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, e), expr_ultimate_method("client", e, m). -local_creation_pinned(local) :- local_creation_init(local), !local_reassigned(local). +local_creation_pinned(local) :- local_creation_init(local), !local_reassigned(local), !local_creation_external(local). +// ...but a creation of a type no staged IR declares pins nothing: `final StringBuilder sb = new StringBuilder()` +// with no JDK staged has no flow type at all, so `sb.toString()` was an unresolved site -- and every unresolved +// `toString` / `append` was a by-name caller of each client method of that name. The declared type is then the +// answer, exactly as for an initializer that is not a creation: an external receiver, a library boundary. +.decl creation_ref_external(ref:symbol) +creation_ref_external(ref) :- java_type_reference(kind, "OBJECT_CREATION_TYPE", _, _, _, _, _, "0", tn, _, _, _, _, _, _, _, _, ref), + ext_ref_kind(kind), + tn != "", + ref_file(ref, tn, fp), + !name_resolves_in_file(tn, fp), + !simple_name_declared(tn), + !ref_names_tvar(ref). +.decl local_creation_external(local:symbol) +local_creation_external(local) :- + java_expression("OBJECT_CREATION", "ROOT", "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, obj), + java_type_reference(_, "OBJECT_CREATION_TYPE", _, _, _, _, _, "0", _, _, _, _, _, _, _, obj, "EXPRESSION", ref), + creation_ref_external(ref). expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "FIELD", _, _, _, _, _, _, _, _, _, e), expr_ultimate_type("client", e, encl), @@ -230,6 +247,20 @@ expr_call_candidate(call, m) :- call_site(call, name, recv), external_target(call, label) :- call_site(call, name, recv), expr_type("external", recv, x), label = cat(x, cat(".", name)). +// (a') the receiver is a TYPE NAME no staged IR declares: a static call on the platform or a dependency, +// `Character.charCount(cp)`, `Objects.equals(a, b)`, `Collections.unmodifiableMap(m)`. A static call never +// dispatches, so no client method runs behind it. The parser writes the receiver as an identifier it read as a +// type (not ambiguous with a variable) with the qualified name its imports give; left unresolved, each one was a +// by-name caller of every client method called `equals`, `valueOf`, `max`, `get`. +.decl external_static_receiver(recv:symbol, q:symbol) +external_static_receiver(recv, q) :- + java_expression("IDENTIFIER_REFERENCE", "RECEIVER", _, _, _, _, _, _, _, _, n, _, _, _, "TYPE", "", _, q, "false", _, _, _, _, _, recv), + q != "", n != "", + !qualified_name_declared(q), + !simple_name_declared(n). +external_target(call, label) :- call_site(call, name, recv), + external_static_receiver(recv, q), + label = cat("external:", cat(q, cat(".", name))). // The parser writes parameterCount as text, so the written arity is compared as text. call_arity(call, pc) :- call_argc(call, n), pc = to_string(n). call_arity(call, "0") :- call_site(call, _, _), !call_has_args(call). diff --git a/graph/test/java/expected/02-anonymous-sam.edges b/graph/test/java/expected/02-anonymous-sam.edges index 8eb9ab1d3..e5812a014 100644 --- a/graph/test/java/expected/02-anonymous-sam.edges +++ b/graph/test/java/expected/02-anonymous-sam.edges @@ -1,10 +1,12 @@ ambiguous_anon anon_new AnonymousClasses#main(String[]) -> - ambiguous_unknown method AnonymousClasses#main(String[]) -> - -ambiguous_unknown method AnonymousClasses$anon:Comparator#compare(String,String) -> - ambiguous_unknown method AnonymousClasses$anon:Runnable#run() -> - ambiguous_unknown method AnonymousClasses$anon:Thread#run() -> - ambiguous_unknown new AnonymousClasses#main(String[]) -> - +boundary_lib method AnonymousClasses#main(String[]) -> external:java.util.List.of +boundary_lib method AnonymousClasses#main(String[]) -> external:java.util.List.sort boundary_lib method AnonymousClasses$anon:Comparator#compare(String,String) -> external:String.length +boundary_lib method AnonymousClasses$anon:Comparator#compare(String,String) -> external:java.lang.Integer.compare boundary_lib method AnonymousClasses$anon:Thread#run() -> external:Thread.getName known_edge method AnonymousClasses#main(String[]) -> AnonymousClasses$anon:Runnable#run() known_edge method AnonymousClasses#main(String[]) -> AnonymousClasses$anon:Thread#run() diff --git a/graph/test/java/expected/03-overloads-and-receivers.edges b/graph/test/java/expected/03-overloads-and-receivers.edges index 0c75dda29..d42d5882b 100644 --- a/graph/test/java/expected/03-overloads-and-receivers.edges +++ b/graph/test/java/expected/03-overloads-and-receivers.edges @@ -1,8 +1,10 @@ ambiguous_anon anon_new testcases.engine.ComplexDispatch#() -> - -ambiguous_unknown method testcases.engine.ComplexDispatch#jdkStatics(String,int,int) -> - ambiguous_unknown new testcases.engine.ComplexDispatch#() -> - boundary_lib method testcases.engine.ComplexDispatch#boxingAndFields(String) -> external:java.util.Map.put boundary_lib method testcases.engine.ComplexDispatch#jdkStatics(String,int,int) -> external:String.length +boundary_lib method testcases.engine.ComplexDispatch#jdkStatics(String,int,int) -> external:java.lang.Integer.parseInt +boundary_lib method testcases.engine.ComplexDispatch#jdkStatics(String,int,int) -> external:java.lang.Math.max +boundary_lib method testcases.engine.ComplexDispatch#jdkStatics(String,int,int) -> external:java.lang.String.valueOf known_edge method testcases.engine.ComplexDispatch#internalOverloads(Throwable) -> testcases.engine.ComplexDispatch#emit(String) known_edge method testcases.engine.ComplexDispatch#internalOverloads(Throwable) -> testcases.engine.ComplexDispatch#log(String) known_edge method testcases.engine.ComplexDispatch#internalOverloads(Throwable) -> testcases.engine.ComplexDispatch#log(String,Throwable) diff --git a/graph/test/java/expected/05-lambda-and-method-refs.edges b/graph/test/java/expected/05-lambda-and-method-refs.edges index c0ac7cc96..65d3231fd 100644 --- a/graph/test/java/expected/05-lambda-and-method-refs.edges +++ b/graph/test/java/expected/05-lambda-and-method-refs.edges @@ -3,6 +3,7 @@ ambiguous_unknown method Lam#viaForEach(String) -> - ambiguous_unknown method Lam#viaParamList(List) -> - ambiguous_unknown new Lam#() -> - ambiguous_unknown new Lam#main(String[]) -> - +boundary_lib method Lam#main(String[]) -> external:java.util.List.add boundary_lib method Lam#main(String[]) -> external:java.util.Map.put boundary_lib method Lam#viaForEach(String) -> external:java.util.Map.values boundary_lib method Lam#viaMethodRef(List) -> external:java.util.List.forEach diff --git a/graph/test/java/expected/12-library-interface-override.edges b/graph/test/java/expected/12-library-interface-override.edges index a75f030e1..0dff072c4 100644 --- a/graph/test/java/expected/12-library-interface-override.edges +++ b/graph/test/java/expected/12-library-interface-override.edges @@ -1,5 +1,6 @@ ambiguous_unknown method LibIfaceOverride#main(String[]) -> - ambiguous_unknown new LibIfaceOverride#main(String[]) -> - +boundary_lib method LibIfaceOverride#main(String[]) -> external:java.util.List.add boundary_lib method LibIfaceOverride#viaComparator(Comparator,String,String) -> external:java.util.Comparator.compare boundary_lib method LibIfaceOverride#viaFunction(Function,String) -> external:java.util.function.Function.apply boundary_lib method LibIfaceOverride#viaLibrarySort(List) -> external:java.util.List.sort diff --git a/graph/test/java/expected/13-type-qualified-statics.edges b/graph/test/java/expected/13-type-qualified-statics.edges index 525d2b29c..c077724bf 100644 --- a/graph/test/java/expected/13-type-qualified-statics.edges +++ b/graph/test/java/expected/13-type-qualified-statics.edges @@ -1,5 +1,6 @@ ambiguous_unknown method TypeQualifiedStatics#main(String[]) -> - -ambiguous_unknown method TypeQualifiedStatics#viaLibraryType(String) -> - +boundary_lib method TypeQualifiedStatics#viaLibraryType(String) -> external:java.lang.Integer.parseInt +boundary_lib method TypeQualifiedStatics#viaLibraryType(String) -> external:java.lang.String.valueOf known_edge method TypeQualifiedStatics#main(String[]) -> TypeQualifiedStatics#viaLibraryType(String) known_edge method TypeQualifiedStatics#main(String[]) -> TypeQualifiedStatics#viaNestedType(String) known_edge method TypeQualifiedStatics#main(String[]) -> TypeQualifiedStatics#viaOwnTypeName(String) diff --git a/graph/test/java/expected/14-lambda-param-callback.edges b/graph/test/java/expected/14-lambda-param-callback.edges index 5477eef86..e25dedab2 100644 --- a/graph/test/java/expected/14-lambda-param-callback.edges +++ b/graph/test/java/expected/14-lambda-param-callback.edges @@ -1,9 +1,9 @@ -ambiguous_unknown method LambdaParamCallback#main(String[]) -> - ambiguous_unknown method LambdaParamCallback#viaMapValues(String) -> - ambiguous_unknown method LambdaParamCallback#viaStream(List,String) -> - ambiguous_unknown method LambdaParamCallback#viaTypedList(List,String) -> - ambiguous_unknown new LambdaParamCallback#() -> - ambiguous_unknown new LambdaParamCallback#main(String[]) -> - +boundary_lib method LambdaParamCallback#main(String[]) -> external:java.util.List.add boundary_lib method LambdaParamCallback#main(String[]) -> external:java.util.Map.put boundary_lib method LambdaParamCallback#viaMapValues(String) -> external:java.util.Map.values boundary_lib method LambdaParamCallback#viaStream(List,String) -> external:java.util.List.stream diff --git a/graph/test/java/expected/20-reflection-blind-spot.edges b/graph/test/java/expected/20-reflection-blind-spot.edges index fb79ed070..80c61dd87 100644 --- a/graph/test/java/expected/20-reflection-blind-spot.edges +++ b/graph/test/java/expected/20-reflection-blind-spot.edges @@ -6,7 +6,9 @@ boundary_lib method ReflectionSites#viaDynamicName(String) -> external:String.su boundary_lib method ReflectionSites#viaDynamicName(String) -> external:java.lang.reflect.Method.invoke boundary_lib method ReflectionSites#viaMethodInvoke(String) -> external:Class.getDeclaredConstructor boundary_lib method ReflectionSites#viaMethodInvoke(String) -> external:Class.getMethod +boundary_lib method ReflectionSites#viaMethodInvoke(String) -> external:java.lang.Class.forName boundary_lib method ReflectionSites#viaMethodInvoke(String) -> external:java.lang.reflect.Method.invoke +boundary_lib method ReflectionSites#viaNewInstance(String) -> external:java.lang.Class.forName boundary_lib method ReflectionSites#viaNewInstance(String) -> external:java.lang.reflect.Constructor.newInstance known_edge method ReflectionSites#main(String[]) -> ReflectionSites#viaDynamicName(String) known_edge method ReflectionSites#main(String[]) -> ReflectionSites#viaMethodInvoke(String) diff --git a/graph/test/java/expected/33-compiler-lowering.edges b/graph/test/java/expected/33-compiler-lowering.edges index f733fc165..2da65b27b 100644 --- a/graph/test/java/expected/33-compiler-lowering.edges +++ b/graph/test/java/expected/33-compiler-lowering.edges @@ -1,12 +1,13 @@ -ambiguous_unknown method lowering.Lowering#explicitRequireNonNull(Sink) -> - -ambiguous_unknown method lowering.Lowering#tryWithResources(String) -> - ambiguous_unknown new lowering.Lowering#tryWithResources(String) -> - ambiguous_unknown new lowering.Lowering.Bag#() -> - boundary_lib method lowering.Lowering#boundRef(List,Sink) -> external:java.util.List.forEach boundary_lib method lowering.Lowering#explicitIterator(Bag) -> external:java.util.Iterator.hasNext boundary_lib method lowering.Lowering#explicitIterator(Bag) -> external:java.util.Iterator.next +boundary_lib method lowering.Lowering#explicitRequireNonNull(Sink) -> external:java.util.Objects.requireNonNull boundary_lib method lowering.Lowering#explicitUnbox(Integer) -> external:Integer.intValue boundary_lib method lowering.Lowering#plainLibraryCall(List) -> external:java.util.List.size +boundary_lib method lowering.Lowering#tryWithResources(String) -> external:java.io.StringReader.read +boundary_lib method lowering.Lowering#tryWithResources(String) -> external:java.lang.String.valueOf boundary_lib method lowering.Lowering#use(String) -> external:String.trim boundary_lib method lowering.Lowering.Bag#iterator() -> external:java.util.List.iterator boundary_lib method lowering.Lowering.Bag#size() -> external:java.util.List.size diff --git a/graph/test/java/expected/48-local-scope-shadowing.edges b/graph/test/java/expected/48-local-scope-shadowing.edges index 87e52222e..506d49b9e 100644 --- a/graph/test/java/expected/48-local-scope-shadowing.edges +++ b/graph/test/java/expected/48-local-scope-shadowing.edges @@ -1,6 +1,6 @@ -ambiguous_unknown method probe.Shadow#catchParameter() -> - ambiguous_unknown new probe.Shadow#catchParameter() -> - boundary_lib method probe.Shadow#catchParameter() -> external:RuntimeException.getMessage +boundary_lib method probe.Shadow#catchParameter() -> external:java.lang.String.valueOf boundary_lib method probe.Shadow#forHeader() -> external:String.isEmpty known_edge method probe.Shadow#after() -> probe.LocalSide#fromLocal() known_edge method probe.Shadow#catchParameter() -> probe.FieldSide#fromField() diff --git a/graph/test/java/expected/59-function-value-through-ternary.edges b/graph/test/java/expected/59-function-value-through-ternary.edges index 9003d6d09..d849e61c3 100644 --- a/graph/test/java/expected/59-function-value-through-ternary.edges +++ b/graph/test/java/expected/59-function-value-through-ternary.edges @@ -1,6 +1,6 @@ -ambiguous_unknown method probe.App#() -> - -ambiguous_unknown method probe.App#callers() -> - +boundary_lib method probe.App#() -> external:java.lang.Boolean.getBoolean boundary_lib method probe.App#callOther(App,String) -> external:java.util.function.Function.apply +boundary_lib method probe.App#callers() -> external:java.lang.Boolean.getBoolean boundary_lib method probe.App#callers() -> external:java.util.function.Function.apply boundary_lib method probe.App#dispatch(String) -> external:java.util.function.Function.apply boundary_lib method probe.App#getPath(String) -> external:String.split diff --git a/graph/test/java/expected/61-var-local-type.edges b/graph/test/java/expected/61-var-local-type.edges index dd0eb4c06..0a171c390 100644 --- a/graph/test/java/expected/61-var-local-type.edges +++ b/graph/test/java/expected/61-var-local-type.edges @@ -6,6 +6,7 @@ ambiguous_unknown method probe.VarLocals#lambdaUnknownInterface() -> - 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#fromUnknown() -> external:java.util.Collections.emptyList boundary_lib method probe.VarLocals#lambdaLocal(Widget) -> external:java.util.function.Consumer.accept boundary_lib method probe.VarLocals#varExternal(Object) -> external:java.text.Format.format known_edge lambda_body probe.VarLocals#lambdaLocal(Widget) -> probe.Widget#paint() diff --git a/graph/test/java/expected/67-framework-registered-entry-points.edges b/graph/test/java/expected/67-framework-registered-entry-points.edges index 7b81a0c8b..dc1b8be83 100644 --- a/graph/test/java/expected/67-framework-registered-entry-points.edges +++ b/graph/test/java/expected/67-framework-registered-entry-points.edges @@ -1,9 +1,9 @@ ambiguous_unknown method app.init.AppInit#onStartup(Set,ServletContext) -> - -ambiguous_unknown method app.rs.Size#valueOf(String) -> - ambiguous_unknown new app.init.BootConfig#bootServlet() -> - boundary_lib method app.init.AppInit#onStartup(Set,ServletContext) -> external:jakarta.servlet.ServletContext.addFilter boundary_lib method app.init.AppInit#onStartup(Set,ServletContext) -> external:jakarta.servlet.ServletContext.addListener boundary_lib method app.init.AppInit#onStartup(Set,ServletContext) -> external:jakarta.servlet.ServletContext.addServlet +boundary_lib method app.rs.Size#valueOf(String) -> external:java.lang.Integer.parseInt callback_registered callback app.init.AppInit#onStartup(Set,ServletContext) -> app.init.InitFilter#doFilter(ServletRequest,ServletResponse,FilterChain) known_edge method app.init.AppInit#wire(EventBus) -> app.init.EventBus#addListener(Class) known_edge new app.beans.AppConfig#plain() -> app.beans.Plain#() diff --git a/graph/test/java/expected/68-http-routes-and-sends.edges b/graph/test/java/expected/68-http-routes-and-sends.edges index 1e17c53b7..6460aa77a 100644 --- a/graph/test/java/expected/68-http-routes-and-sends.edges +++ b/graph/test/java/expected/68-http-routes-and-sends.edges @@ -1,9 +1,9 @@ -ambiguous_unknown method app.rs.RsClients#() -> - ambiguous_unknown method app.rs.RsClients#order(String) -> - ambiguous_unknown method app.rs.RsClients#orders() -> - ambiguous_unknown new app.rs.RsClients#() -> - ambiguous_unknown new app.spring.OrderClient#() -> - ambiguous_unknown new app.web.WebCaller#() -> - +boundary_lib method app.rs.RsClients#() -> external:jakarta.ws.rs.client.ClientBuilder.newClient boundary_lib method app.rs.RsClients#gadget(String) -> external:org.springframework.web.client.RestTemplate.getForObject boundary_lib method app.rs.RsClients#item(String) -> external:org.springframework.web.client.RestTemplate.getForObject boundary_lib method app.rs.RsClients#widget(String) -> external:org.springframework.web.client.RestTemplate.getForObject diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 9f3a41007..fe93b1f15 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -803,6 +803,42 @@ class Impact: } IMPORT_RE['.tsx'] = IMPORT_RE['.js'] = IMPORT_RE['.jsx'] = IMPORT_RE['.mjs'] = IMPORT_RE['.cjs'] = IMPORT_RE['.ts'] + def java_static_members(self): + """(m,) for each Java member that belongs to the class, not to an instance: a static method, a static field, an enum + constant, an interface's field. Using one of these is what initializes its type (JLS 12.4.1); calling an instance + member is not, the instance having been built (and the type initialized) before.""" + g = self.g; out = set() + if not (g.has('methods') and g.has('fields')): return [] + static_m = {r['id'] for r in g.q("SELECT id FROM methods WHERE kind = 'STATIC_METHOD' AND file_path LIKE '%.java'")} + iface = {r['id'] for r in g.q("SELECT id FROM types WHERE category LIKE '%INTERFACE%'")} if g.has('types') else set() + static_f = {r['id'] for r in g.q("SELECT id, modifiers, kind, owner_type_id FROM fields WHERE file_path LIKE '%.java'") + if 'STATIC' in (r['modifiers'] or '') or r['kind'] == 'enum_constant' or r['owner_type_id'] in iface} + out = {(i,) for i, s in g.sym.items() if s.get('method_id') in static_m} + out |= {(f['id'],) for f in self.fields.values() if f.get('id') in static_f} + return sorted(out) + + 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 + initialized; an instance field's runs in every constructor. Read off the line of the call against the spans of the + type's fields; a line in no field's span is counted static, as it was before the two were told apart.""" + g = self.g + if not (g.has('fields') and g.has('call_sites')): return [] + types = {i for i, s in g.sym.items() if s.get('type_id') and not s.get('method_id') and (s.get('file') or '').endswith('.java')} + if not types: return [] + iface = {r['id'] for r in g.q("SELECT id FROM types WHERE category LIKE '%INTERFACE%'")} if g.has('types') else set() + spans = collections.defaultdict(list) + for r in g.q("SELECT owner_type_id t, modifiers m, kind k, start_line a, end_line b FROM fields WHERE owner_type_id IS NOT NULL AND start_line > 0"): + st = 'STATIC' in (r['m'] or '') or r['k'] == 'enum_constant' or r['t'] in iface + spans[r['t']].append((r['a'], r['b'] or r['a'], 'static' if st else 'instance')) + out = set() + for r in g.q("SELECT s.caller_id t, e.callee_method_id b, s.start_line l FROM call_sites s JOIN call_edges e ON e.call_site_id = s.id " + "WHERE e.callee_method_id IS NOT NULL AND s.caller_id LIKE 'TYPE_REGISTRY%'"): + if r['t'] not in types: continue + hit = [k for a, b, k in spans.get(r['t'], ()) if a <= (r['l'] or 0) <= b] + out.add((r['t'], r['b'], 'instance' if hit and all(k == 'instance' for k in hit) else 'static')) + return sorted(out) + def imports(self): """(file, file) for every import one source file makes of another IN THIS REPOSITORY.""" g = self.g @@ -1343,7 +1379,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 = '69' # 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 = '71' # 71: walk_edge, no `defines` hop out of a Java initializer (a member written in it is not run by it); 70: 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; 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 @@ -1466,6 +1502,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()) 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 @@ -3082,7 +3119,11 @@ def main(argv): # `decorator by name` is the rules' own hop from a decorated def to the decorator it names # (dl/impact.dl fw_edge), asserted exactly as `decorator` is; leaving it out printed "✗ not found" on # every answer whose test route passes through a project decorator - if (a, b) not in real and not any(x == b and t in ('dispatch', 'defines', 'by key', 'decorator', 'decorator by name', 'remote', 'spawns', 'filter', 'library callback') for x, t in parent.get(a, [])): bad += 1 + # `at load`, `protocol` and `at import` are the rules' own hops too (load_hop, protocol_hop, import_hop): the + # JVM runs an initializer, a library runs equals / toString, an import runs a module body, and none of them + # is a call_edges row. Left out, every answer whose chain crossed a static initializer printed "✗ not + # found: report this, do not use the answer" for edges the rules derived exactly as designed + if (a, b) not in real and not any(x == b and t in ('dispatch', 'defines', 'by key', 'decorator', 'decorator by name', 'remote', 'spawns', 'filter', 'library callback', 'at load', 'protocol', 'at import') for x, t in parent.get(a, [])): bad += 1 asserted = collections.defaultdict(set) # the callee each direct row claims an edge to for c, callee in {(x[0], x[1]) for x in res['direct_edge']}: asserted[c].add(callee) for c, role, why, cert, loc, n in D: diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index 6c5f1978c..f6a52d4cc 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -886,7 +886,19 @@ 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), 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, _), d < 40. +// 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 , +// or the TYPE the engine names as the caller of a field initializer) handed the method to the initializer, and the +// `at load` hop then to every user of the type. A `defines` hop from a method body is kept: a lambda written in a method usually runs while it does. +.decl init_defines(a:symbol, b:symbol) +init_defines(a, b) :- edge(a, b, "defines"), java_decl(a), member(_, a, "", _). +init_defines(a, b) :- edge(a, b, "defines"), java_decl(a), member(_, a, "", _). +init_defines(a, b) :- edge(a, b, "defines"), java_decl(a), typ(a, _, _). +.decl walk_edge(a:symbol, b:symbol, t:symbol) +walk_edge(a, b, t) :- edge(a, b, t), t != "defines". +walk_edge(a, b, "defines") :- edge(a, b, "defines"), !init_defines(a, b). up(q, a, d+1) :- up(q, b, d), fw_edge(a, b, _), d < 40. // ── WHAT ONE INSTANCE WAS GIVEN: the edge holds for some receivers only ────────────────────────────────────────── // `new Bus({ validate: check })` and `b.on(handleA)` give ONE bus a callback, and `Bus.emit` calls it through what @@ -926,7 +938,7 @@ up_all(q, m, d) :- upg(q, m, _, _, d). reach(q, m, d) :- up_all(q, m, d), d = min x : up_all(q, m, x). // the chain read-back: a is one hop further from the change than b, through edge a → b .decl parent_up(q:symbol, a:symbol, b:symbol, t:symbol) -parent_up(q, a, b, t) :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d - 1, edge(a, b, t). +parent_up(q, a, b, t) :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d - 1, walk_edge(a, b, t). parent_up(q, a, b, t) :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d - 1, fw_edge(a, b, t). // The import hop is taken from a MODULE BODY, and only when that module body is reached by edges which actually // RUN. `up` walks `defines` as well, which is right for it — a change to an inner function does concern the @@ -971,7 +983,7 @@ strong_direct(q, c) :- direct(q, c, _, _, "resolved", _, _). strong_direct(q, c) :- direct(q, c, _, _, "in scope", _, _). .decl up_sure(q:symbol, m:symbol, d:number) up_sure(q, m, 0) :- seed_sure(q, m). -up_sure(q, a, d+1) :- up_sure(q, b, d), edge(a, b, _), d < 40. +up_sure(q, a, d+1) :- up_sure(q, b, d), walk_edge(a, b, _), d < 40. .decl reach_sure(q:symbol, m:symbol) reach_sure(q, m) :- up_sure(q, m, _). .output reach_sure @@ -1148,17 +1160,35 @@ parent_up(q, a, b, "protocol") :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d // The type's own members count too: once its initialization has failed, every one of them fails with it. // Taken only where the walk ARRIVES at the type as a caller (d > 0): a type that is the change itself, or the owner // of a changed field, is a seed for other reasons, and its users are already the direct rows of those rules. -.decl init_runner(r:symbol, t:symbol) -init_runner(t, t) :- typ(t, _, _), java_decl(t), edge(t, _, _). -init_runner(c, t) :- member(t, c, "", _), java_decl(c). -init_runner(c, t) :- member(t, c, "", _), java_decl(c). -.decl init_use(a:symbol, t:symbol) -init_use(a, t) :- calls(a, m, tier, _, _), tier != "stub", owner(m, t). -init_use(a, t) :- calls(a, m, tier, _, _), tier != "stub", kind(m, "constructor"), owner(m, s), extends(s, t). -init_use(a, t) :- faccess(a, fl, _, _, _, _), field(fl, t, _, _, _). +// +// WHICH INITIALIZER, AND WHAT RUNS IT. A static initializer (a static field's or enum constant's initializer, a +// `static { }` block) runs once, when the type is initialized, and what initializes a type is fixed by JLS 12.4.1: +// a `new` of it or of a subtype, a call of one of its STATIC methods, a use of one of its static fields. Calling an +// instance method or reading an instance field is not one: the instance exists, so its type was initialized before. +// Taking the hop from every user of the type named everything that touched one instance. An INSTANCE initializer (an instance field's initializer, an `{ }` block) runs inside every constructor of the +// type and of its subtypes (through their super() call): its hop goes to those constructors, and the walk takes +// their callers from there. +// jstatic(m) m belongs to the class: a static method or field, an enum constant, an interface field +// init_call(t, b, k) the type t calls b from a field initializer of kind k (static | instance) +.decl jstatic(m:symbol) .input jstatic +.decl init_call(t:symbol, b:symbol, k:symbol) .input init_call +.decl init_trigger(a:symbol, t:symbol) +init_trigger(a, t) :- calls(a, m, tier, _, _), tier != "stub", owner(m, t), jstatic(m). +init_trigger(a, t) :- calls(a, m, tier, _, _), tier != "stub", owner(m, t), kind(m, "constructor"). +init_trigger(a, t) :- calls(a, m, tier, _, _), tier != "stub", kind(m, "constructor"), owner(m, s), extends(s, t). +init_trigger(a, t) :- faccess(a, fl, _, _, _, _), field(fl, t, _, _, _), jstatic(fl). +.decl init_ctor(i:symbol, t:symbol) +init_ctor(i, t) :- java_decl(t), kind(i, "constructor"), owner(i, t). +init_ctor(i, t) :- java_decl(t), extends(s, t), kind(i, "constructor"), owner(i, s). +// the type reached as a caller, through an initializer of kind k (the callee it calls there is in the walk) +.decl type_init(q:symbol, t:symbol, k:symbol) +type_init(q, t, k) :- up(q, t, d), d > 0, typ(t, _, _), java_decl(t), walk_edge(t, b, _), up(q, b, _), init_call(t, b, k). +type_init(q, t, "static") :- up(q, t, d), d > 0, typ(t, _, _), java_decl(t), walk_edge(t, b, _), up(q, b, _), !init_call(t, b, _). .decl load_hop(q:symbol, a:symbol, r:symbol) -load_hop(q, a, r) :- up(q, r, d), d > 0, init_runner(r, t), r = t, init_use(a, t), a != r. -load_hop(q, a, r) :- up(q, r, _), init_runner(r, t), r != t, init_use(a, t), a != r. +load_hop(q, a, t) :- type_init(q, t, "static"), init_trigger(a, t), a != t. +load_hop(q, i, t) :- type_init(q, t, "instance"), init_ctor(i, t), i != t. +load_hop(q, a, r) :- up(q, r, _), member(t, r, "", _), java_decl(r), init_trigger(a, t), a != r. +load_hop(q, i, r) :- up(q, r, _), member(t, r, "", _), java_decl(r), init_ctor(i, t), i != r. up(q, a, d+1) :- up(q, r, d), load_hop(q, a, r), d < 40. parent_up(q, a, b, "at load") :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d - 1, load_hop(q, a, b). // WHAT RUNS AT IMPORT. `settings = Settings()` at the top level of a module is executed by the import statement, diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/case.json b/tests/cases/java/initializer-does-not-run-what-it-defines/case.json new file mode 100644 index 000000000..79f5aa1b1 --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/case.json @@ -0,0 +1,10 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "a method of an anonymous class written in a static block runs when it is called on the object, not when the block runs: a test that only initializes the type does not reach it", + "run": ["impact", "Upper.of", "--tests"], + "want": ["PromoterTest::promotes"], + "avoid": ["PeekTest"]}, + {"why": "control: what the static block itself calls still reaches every test that initializes the type", + "run": ["impact", "Tables.prime", "--tests"], + "want": ["PeekTest::peeks", "PromoterTest::promotes"], + "avoid": ["0 of"]}]} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Access.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Access.java new file mode 100644 index 000000000..7e34ff378 --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Access.java @@ -0,0 +1,5 @@ +package pkg; + +public interface Access { + String promote(String s); +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PeekTest.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PeekTest.java new file mode 100644 index 000000000..fb2f81c37 --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PeekTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class PeekTest { + @Test + public void peeks() { assert Reader.peek().equals("peek"); } +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Promoter.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Promoter.java new file mode 100644 index 000000000..08345220b --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Promoter.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Promoter { + private Promoter() { } + + static String promote(String s) { return Reader.ACCESS.promote(s); } +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PromoterTest.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PromoterTest.java new file mode 100644 index 000000000..e8665eabe --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/PromoterTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class PromoterTest { + @Test + public void promotes() { assert Promoter.promote("a").equals("A"); } +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Reader.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Reader.java new file mode 100644 index 000000000..6d830c84d --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Reader.java @@ -0,0 +1,16 @@ +package pkg; + +public class Reader { + static Access ACCESS; + + static { + // the initializer builds the accessor; its method runs only when someone calls it + ACCESS = new Access() { + @Override + public String promote(String s) { return Upper.of(s); } + }; + Tables.prime(); + } + + public static String peek() { return "peek"; } +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Tables.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Tables.java new file mode 100644 index 000000000..189703331 --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Tables.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Tables { + private Tables() { } + + static void prime() { } +} diff --git a/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Upper.java b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Upper.java new file mode 100644 index 000000000..64513dfae --- /dev/null +++ b/tests/cases/java/initializer-does-not-run-what-it-defines/src/pkg/Upper.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Upper { + private Upper() { } + + static String of(String s) { return s.toUpperCase(); } +} diff --git a/tests/cases/java/overload-by-operator-argument/case.json b/tests/cases/java/overload-by-operator-argument/case.json new file mode 100644 index 000000000..56d859f90 --- /dev/null +++ b/tests/cases/java/overload-by-operator-argument/case.json @@ -0,0 +1,18 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "an arithmetic argument (`all.size() - 1`) is a number, so it never reaches the varargs Node constructor", + "run": ["impact", "Sizer.shrink", "--depends"], + "want": ["Bag.Bag"], + "avoid": ["one of a set"]}, + {"why": "`--i` is a number: drop(String) is not a candidate", + "run": ["impact", "Sizer.rewind", "--depends"], + "want": ["Bag.drop"], + "avoid": ["one of a set"]}, + {"why": "a comparison is a boolean: flag(Node) is not a candidate", + "run": ["impact", "Sizer.mark", "--depends"], + "want": ["Bag.flag"], + "avoid": ["one of a set"]}, + {"why": "control: a Node argument reaches the Node overload and not the int one", + "run": ["impact", "Sizer.wrap", "--depends"], + "want": ["Bag.Bag"], + "avoid": ["one of a set"]}]} diff --git a/tests/cases/java/overload-by-operator-argument/src/pkg/Bag.java b/tests/cases/java/overload-by-operator-argument/src/pkg/Bag.java new file mode 100644 index 000000000..9297d3005 --- /dev/null +++ b/tests/cases/java/overload-by-operator-argument/src/pkg/Bag.java @@ -0,0 +1,20 @@ +package pkg; + +import java.util.ArrayList; +import java.util.List; + +public class Bag { + private final List nodes = new ArrayList<>(); + + public Bag(int capacity) { } + + public Bag(Node... nodes) { } + + public void drop(int index) { nodes.remove(index); } + + public void drop(String name) { } + + public void flag(boolean on) { } + + public void flag(Node node) { } +} diff --git a/tests/cases/java/overload-by-operator-argument/src/pkg/Node.java b/tests/cases/java/overload-by-operator-argument/src/pkg/Node.java new file mode 100644 index 000000000..c7ee44f62 --- /dev/null +++ b/tests/cases/java/overload-by-operator-argument/src/pkg/Node.java @@ -0,0 +1,3 @@ +package pkg; + +public class Node { } diff --git a/tests/cases/java/overload-by-operator-argument/src/pkg/Sizer.java b/tests/cases/java/overload-by-operator-argument/src/pkg/Sizer.java new file mode 100644 index 000000000..81ecc6067 --- /dev/null +++ b/tests/cases/java/overload-by-operator-argument/src/pkg/Sizer.java @@ -0,0 +1,15 @@ +package pkg; + +import java.util.List; + +public class Sizer { + // the arguments are untyped calls into the platform, but the operator says what they are + Bag shrink(List all) { return new Bag(all.size() - 1); } + + void rewind(Bag b, int i) { b.drop(--i); } + + void mark(Bag b, List all) { b.flag(all.size() > 1); } + + // control: a node argument still reaches the reference overloads + Bag wrap(Node n) { return new Bag(n); } +} diff --git a/tests/cases/java/overload-by-operator-argument/src/pkg/SizerTest.java b/tests/cases/java/overload-by-operator-argument/src/pkg/SizerTest.java new file mode 100644 index 000000000..faa2972c3 --- /dev/null +++ b/tests/cases/java/overload-by-operator-argument/src/pkg/SizerTest.java @@ -0,0 +1,18 @@ +package pkg; + +import java.util.Collections; +import org.junit.Test; + +public class SizerTest { + @Test + public void shrinks() { assert new Sizer().shrink(Collections.emptyList()) != null; } + + @Test + public void rewinds() { new Sizer().rewind(new Bag(1), 1); } + + @Test + public void marks() { new Sizer().mark(new Bag(1), Collections.emptyList()); } + + @Test + public void wraps() { assert new Sizer().wrap(new Node()) != null; } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/case.json b/tests/cases/java/platform-receiver-is-a-boundary/case.json new file mode 100644 index 000000000..1f2ac670e --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/case.json @@ -0,0 +1,18 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "with no JDK staged, `final StringBuilder sb = new StringBuilder(); sb.toString()` is a call on a platform type (the creation's type is no staged declaration), not an untyped call that may be any client toString", + "run": ["impact", "Report.toString", "--tests"], + "want": ["ReportTest::prints"], + "avoid": ["FormatterTest", "PainterTest"]}, + {"why": "a static call on a platform type (`Math.max`) never runs a client method of that name", + "run": ["impact", "Report.max", "--tests"], + "want": ["PainterTest::clamps"], + "avoid": ["FormatterTest"]}, + {"why": "control: a local of a client type built with `new` is still pinned to the type built", + "run": ["impact", "Square.area", "--tests"], + "want": ["0 of"], + "avoid": ["PainterTest"]}, + {"why": "control: the pinned local still reaches the type built", + "run": ["impact", "Circle.area", "--tests"], + "want": ["PainterTest::paints"], + "avoid": ["FormatterTest"]}]} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Circle.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Circle.java new file mode 100644 index 000000000..b770d6df9 --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Circle.java @@ -0,0 +1,5 @@ +package pkg; + +public class Circle implements Shape { + public double area() { return 3.0; } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Formatter.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Formatter.java new file mode 100644 index 000000000..a90ff57dd --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Formatter.java @@ -0,0 +1,13 @@ +package pkg; + +public class Formatter { + // a local of a platform type built with `new`: its calls are the platform's, never a client toString + String render(String s) { + final StringBuilder sb = new StringBuilder(); + sb.append(s); + return sb.toString(); + } + + // a static call on a platform type: never a client max + int widest(int a, int b) { return Math.max(a, b); } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/FormatterTest.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/FormatterTest.java new file mode 100644 index 000000000..644a6c138 --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/FormatterTest.java @@ -0,0 +1,11 @@ +package pkg; + +import org.junit.Test; + +public class FormatterTest { + @Test + public void renders() { assert new Formatter().render("x").equals("x"); } + + @Test + public void widens() { assert new Formatter().widest(1, 2) == 2; } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Painter.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Painter.java new file mode 100644 index 000000000..9652045e5 --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Painter.java @@ -0,0 +1,12 @@ +package pkg; + +public class Painter { + // control: a local of a CLIENT type built with `new` stays pinned to what was built + double paint() { + final Shape s = new Circle(); + return s.area(); + } + + // control: a client static call keeps its edge + int clamp(int a) { return Report.max(a, 0); } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/PainterTest.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/PainterTest.java new file mode 100644 index 000000000..92bdf3edc --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/PainterTest.java @@ -0,0 +1,11 @@ +package pkg; + +import org.junit.Test; + +public class PainterTest { + @Test + public void paints() { assert new Painter().paint() > 0; } + + @Test + public void clamps() { assert new Painter().clamp(-1) == 0; } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Report.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Report.java new file mode 100644 index 000000000..5faf6131c --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Report.java @@ -0,0 +1,13 @@ +package pkg; + +public class Report { + private final String title; + + public Report(String title) { this.title = title; } + + @Override + public String toString() { return "Report " + title; } + + // a client static method that shares its name with a platform one + public static int max(int a, int b) { return a >= b ? a : b; } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/ReportTest.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/ReportTest.java new file mode 100644 index 000000000..fab1eebda --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/ReportTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class ReportTest { + @Test + public void prints() { assert new Report("a").toString().endsWith("a"); } +} diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Shape.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Shape.java new file mode 100644 index 000000000..37c5e315f --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Shape.java @@ -0,0 +1,3 @@ +package pkg; + +public interface Shape { double area(); } diff --git a/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Square.java b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Square.java new file mode 100644 index 000000000..075a16b62 --- /dev/null +++ b/tests/cases/java/platform-receiver-is-a-boundary/src/pkg/Square.java @@ -0,0 +1,5 @@ +package pkg; + +public class Square implements Shape { + public double area() { return 4.0; } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/case.json b/tests/cases/java/type-initializer-only-from-what-initializes-it/case.json new file mode 100644 index 000000000..5ea566daf --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/case.json @@ -0,0 +1,68 @@ +{ + "lang": "java", + "src": "src", + "checks": [ + { + "why": "a static field initializer runs when the type is initialized: a static method call and a constructor call do it (JLS 12.4.1), a call of an instance method on an instance someone else built does not", + "run": [ + "impact", + "Seeds.load", + "--tests" + ], + "want": [ + "CatalogTest::opens", + "RowTest::reads" + ], + "avoid": [ + "HolderTest", + "0 of" + ] + }, + { + "why": "an instance field initializer runs inside the type's constructors only: a static method that builds no instance never runs it", + "run": [ + "impact", + "Names.fresh", + "--tests" + ], + "want": [ + "CatalogTest::opens" + ], + "avoid": [ + "RowTest", + "HolderTest", + "0 of" + ] + }, + { + "why": "control: the instance method's own callers are unchanged", + "run": [ + "impact", + "Catalog.name", + "--tests" + ], + "want": [ + "CatalogTest::opens", + "HolderTest::readsTheFirstName" + ], + "avoid": [ + "RowTest", + "0 of" + ] + }, + { + "why": "a chain through an initializer is verified as the rules' own hop, not reported as an edge missing from the graph", + "run": [ + "impact", + "Seeds.load", + "--tests" + ], + "want": [ + "all present" + ], + "avoid": [ + "not found: report this" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Catalog.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Catalog.java new file mode 100644 index 000000000..8bb026a50 --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Catalog.java @@ -0,0 +1,20 @@ +package pkg; + +import java.util.List; +import java.util.Map; + +public final class Catalog { + // static initializer: runs when Catalog is initialized (new, a static method, a static field) + static final Map ROWS = Seeds.load(); + + // instance initializer: runs inside every constructor of Catalog + private final List names = Names.fresh(); + + public Catalog() { } + + public static Catalog open() { return new Catalog(); } + + public static String row(String key) { return ROWS.get(key); } + + public String name(int i) { return names.get(i); } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/CatalogTest.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/CatalogTest.java new file mode 100644 index 000000000..b87f8944b --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/CatalogTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class CatalogTest { + @Test + public void opens() { assert Catalog.open().name(0).equals("a"); } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Holder.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Holder.java new file mode 100644 index 000000000..5b5c34cf9 --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Holder.java @@ -0,0 +1,9 @@ +package pkg; + +public final class Holder { + private Holder() { } + + // only calls an INSTANCE method of a Catalog it is handed: the type was initialized, and the instance built, + // by whoever made it + static String first(Catalog c) { return c.name(0); } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/HolderTest.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/HolderTest.java new file mode 100644 index 000000000..2c6fc7148 --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/HolderTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class HolderTest { + @Test + public void readsTheFirstName() { assert Holder.first(null) == null; } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Names.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Names.java new file mode 100644 index 000000000..54663d323 --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Names.java @@ -0,0 +1,10 @@ +package pkg; + +import java.util.Arrays; +import java.util.List; + +public final class Names { + private Names() { } + + static List fresh() { return Arrays.asList("a", "b"); } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/RowTest.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/RowTest.java new file mode 100644 index 000000000..a58168b5d --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/RowTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class RowTest { + @Test + public void reads() { assert "v".equals(Catalog.row("k")); } +} diff --git a/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Seeds.java b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Seeds.java new file mode 100644 index 000000000..712376543 --- /dev/null +++ b/tests/cases/java/type-initializer-only-from-what-initializes-it/src/pkg/Seeds.java @@ -0,0 +1,10 @@ +package pkg; + +import java.util.Collections; +import java.util.Map; + +public final class Seeds { + private Seeds() { } + + static Map load() { return Collections.singletonMap("k", "v"); } +}