From 8061569697f173f3b01755a7465db78cc312f411 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:35:44 -0700 Subject: [PATCH 1/4] impact: a Java type's initializer is run by whoever uses the type A call written in a field initializer has no method around it, so the engine names the type as its caller, and a static or instance block is a / callable. Nothing calls either, so the upward walk stopped there: a factory whose `INSTANCE = new Factory()` throws breaks every test that touches the factory, and none of them was selected. impact.dl now hops from such an initializer (a type reached as a caller, or a / ) to whoever uses the type in a way that runs it: a call to one of its members, a construction of a subtype, a read or write of one of its fields. The hop is taken only where the walk arrives at the type as a caller, never from a type or field target's own seed. Test routes through it carry a new rung, `at load`, ranked beside `at import`. The SQL fast path declines a closure that reaches such an initializer. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/ax_evidence.py | 2 +- .../skills/axiomcode/scripts/axiomcode-impact | 6 ++--- .../skills/axiomcode/scripts/dl/impact.dl | 23 +++++++++++++++++++ .../skills/axiomcode/scripts/graph_sql.py | 18 +++++++++++++++ .../case.json | 18 +++++++++++++++ .../src/pkg/Boot.java | 7 ++++++ .../src/pkg/Cache.java | 7 ++++++ .../src/pkg/Client.java | 8 +++++++ .../src/pkg/ClientTest.java | 8 +++++++ .../src/pkg/Defaults.java | 15 ++++++++++++ .../src/pkg/Engine.java | 9 ++++++++ .../src/pkg/EngineTest.java | 8 +++++++ .../src/pkg/Orphan.java | 8 +++++++ .../src/pkg/OtherTest.java | 9 ++++++++ .../src/pkg/Registry.java | 12 ++++++++++ .../src/pkg/RegistryTest.java | 8 +++++++ .../src/pkg/Unused.java | 5 ++++ 17 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/case.json create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Boot.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Cache.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Client.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/ClientTest.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Defaults.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Engine.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/EngineTest.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Orphan.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/OtherTest.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Registry.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/RegistryTest.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Unused.java diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_evidence.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_evidence.py index 4a625ae2..89ca7349 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_evidence.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_evidence.py @@ -34,7 +34,7 @@ # a certainty that is an exact edge (or not a row about a call at all): no evidence EXACT = {None, '', 'resolved', 'sound', 'entry', 'defines', 'defines (not a call)', 'must change', 'alongside', - 'stubs it', 'at import', 'decorator', 'test'} + 'stubs it', 'at import', 'at load', 'decorator', 'test'} def _env(k): diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 7a8dc195..c007571d 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -2884,7 +2884,7 @@ def main(argv): # absorbed: a front end that gains a tier should show up here the first time anyone queries, not go # quietly into the bottom rung forever. UNKNOWN_TIERS = set() - CERT_UP = {'known_edge': 'sound', 'library': 'sound', 'written': 'sound', 'multi_inferred': 'one of a set', 'dispatch': 'dispatch', 'defines': 'defines', 'by key': 'by key', 'spawns': 'spawns', 'decorator': 'sound', 'decorator by name': 'decorator by name', 'at import': 'at import', 'protocol': 'protocol', + CERT_UP = {'known_edge': 'sound', 'library': 'sound', 'written': 'sound', 'multi_inferred': 'one of a set', 'dispatch': 'dispatch', 'defines': 'defines', 'by key': 'by key', 'spawns': 'spawns', 'decorator': 'sound', 'decorator by name': 'decorator by name', 'at import': 'at import', 'at load': 'at load', 'protocol': 'protocol', # handed over as a value and invoked by whoever holds it (`callback_registered`), or emitted # here and handled there (`event_dispatch`). Real engine edges, so `by name` understates them; # registrations rather than resolutions, so `one of a set` — which claims a resolved target @@ -2941,7 +2941,7 @@ def main(argv): # is not the `fixture` rung it used to be labelled as — a fixture route is weak for exactly the reason this one # is strong. It sits below `one of a set` all the same, because it is not a resolved call and should not borrow # the word that means one. - CERT_RANK = {'sound': 0, 'defines': 1, 'at import': 2, 'one of a set': 3, 'dispatch': 4, 'registered': 5, + CERT_RANK = {'sound': 0, 'defines': 1, 'at import': 2, 'at load': 2, 'one of a set': 3, 'dispatch': 4, 'registered': 5, 'remote': 6, 'spawns': 7, 'by key': 8, 'decorator by name': 9, 'protocol': 10, 'fixture': 11, 'by name': 12} # `remote` (#1293) sits just above `by key`: both join two ends on a string, but the engine matched a remote @@ -3489,7 +3489,7 @@ def main(argv): # 180k answer on a hub method -- 435 tests, 433 of them through routes this answer itself calls weak -- which # no agent can read inline, so it was saved to a file and re-read piecemeal. Every test is still accounted for: # the strong ones by name here, all of them by rung and file under --tests, each route under --why. - STRONG = ('sound', 'defines', 'at import', 'one of a set') + STRONG = ('sound', 'defines', 'at import', 'at load', 'one of a set') tname = lambda m: f"{g.sym[m]['owner'] or os.path.basename(g.sym[m]['file'])}::{g.sym[m]['name']}" torder = lambda m: (tests[m][0], g.sym[m]['display'], g.loc(m), m) def route(m): diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index f8d70e36..7ca44f79 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -1103,6 +1103,29 @@ constructs(c, t) :- member(t, i, "__init__", _), edge(c, i, _), c != i. protocol_hop(q, c, m) :- seed(q, m), protocol_member(t, m), constructs(c, t), c != m. up(q, c, 1) :- protocol_hop(q, c, _). parent_up(q, a, b, "protocol") :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d - 1, protocol_hop(q, a, b). +// WHAT RUNS WHEN A TYPE IS LOADED OR BUILT (Java). A call written in a field initializer has no method around it, so +// the engine names the TYPE as its caller (call_chain.dl `call_from`), and a `static { }` / `{ }` block is a +// / callable. Nothing calls either: the JVM runs them when the type is first used or an +// instance is built. So the walk stopped at the type, and a factory whose `INSTANCE = new Factory()` throws — which +// breaks every test that touches the factory, by an ExceptionInInitializerError — selected none of them. +// The hop goes from that initializer to whoever uses the type in a way that runs it: calls one of its members (a +// static method, a constructor, a method of an instance), constructs a subtype, or reads or writes one of its fields. +// 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, _, _, _). +.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. +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, // so breaking Settings.__init__ breaks importing that module, and with it every module that imports it and every // test in those files. The graph records the module body's call to the constructor and records nothing at all about diff --git a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py index ecf87c75..1e7d53e7 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py @@ -3571,6 +3571,21 @@ def _has_framework_hops(q, at=None, site_file=None): return True return False +def _reaches_init(q, depth): + """True when the closure holds a Java initializer the rules hop on from (load_hop): a type reached as a caller, or a + / callable""" + if not depth or not _has(q, 'symbols'): return False + ids = [m for m, d in depth.items() if d > 0 and m.startswith('TYPE_')] + if ids and q("SELECT 1 FROM call_edges WHERE caller_id IN ({}) LIMIT 1".format(','.join('?' * len(ids))), *ids): + return True + ids = list(depth) + for i in range(0, len(ids), 900): + part = ids[i:i + 900] + if q("SELECT 1 FROM symbols WHERE id IN ({}) AND name IN ('', '') AND file LIKE '%.java' LIMIT 1" + .format(','.join('?' * len(part))), *part): + return True + return False + def _spawn_edges(q, lines, at): """fw_edge(a, b, "spawns") :- spawns_fact(a, b): a test that runs a script by its path (ax_spawn.py)""" if lines is None or at is None or not _has(q, 'symbols'): return [] @@ -3868,6 +3883,9 @@ def solve_from_targets(q, T, QS, site_file=None, nonsource=(), code=None, at=Non out['direct_edge'] += [[c, m, qq] for c, m in _de] out['seed_byname'] += [[c, qq] for c in byname] depth = reach_from(rev, seeds, byname, gate=gate) + # the `at load` hop (dl/impact.dl load_hop) is not ported: a closure that arrives at a Java type as a caller + # (a field initializer) or at a / goes on to the type's users there, so decline + if _reaches_init(q, depth): return None out['reach'] += [[m, str(d), qq] for m, d in depth.items()] # reach_sure: the same closure from the seeds that are an exact edge only — a seed reached ONLY through a # by-name / text / one-of-a-set dependent is weak, and the answer says how much of itself rests on those diff --git a/tests/cases/java/type-initializer-runs-for-its-users/case.json b/tests/cases/java/type-initializer-runs-for-its-users/case.json new file mode 100644 index 00000000..211f16bc --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/case.json @@ -0,0 +1,18 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "a method called only from a static field initializer runs when the type is first used, so a test that calls a static method of that type is selected", + "run": ["impact", "Defaults.build", "--tests"], + "want": ["RegistryTest::findsKey"], + "avoid": ["OtherTest", "ClientTest", "0 of"]}, + {"why": "a method called from an instance field initializer runs in every construction, so a test that builds the type is selected", + "run": ["impact", "Cache.create", "--tests"], + "want": ["ClientTest::readsThroughTheCache"], + "avoid": ["RegistryTest", "EngineTest"]}, + {"why": "a method called from a static block runs when the type is first used", + "run": ["impact", "Boot.init", "--tests"], + "want": ["EngineTest::runsAfterBoot"], + "avoid": ["RegistryTest", "ClientTest"]}, + {"why": "control: the initializer of a type no test uses selects no test", + "run": ["impact", "Unused.compute", "--tests"], + "want": ["0 of"], + "avoid": ["RegistryTest", "ClientTest", "EngineTest", "OtherTest"]}]} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Boot.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Boot.java new file mode 100644 index 00000000..1a3d00c4 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Boot.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Boot { + static int ready; + + static void init() { ready = 1; } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Cache.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Cache.java new file mode 100644 index 00000000..2909d424 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Cache.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Cache { + static Cache create() { return new Cache(); } + + String get() { return "v"; } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Client.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Client.java new file mode 100644 index 00000000..6ec2d58a --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Client.java @@ -0,0 +1,8 @@ +package pkg; + +public class Client { + // an instance field initializer runs in every constructor of Client + private final Cache cache = Cache.create(); + + public String read() { return cache.get(); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/ClientTest.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/ClientTest.java new file mode 100644 index 00000000..aab102ad --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/ClientTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class ClientTest { + @Test + public void readsThroughTheCache() { assert "v".equals(new Client().read()); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Defaults.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Defaults.java new file mode 100644 index 00000000..bf4490b5 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Defaults.java @@ -0,0 +1,15 @@ +package pkg; + +import java.util.HashMap; +import java.util.Map; + +public final class Defaults { + private Defaults() { } + + // runs only from Registry's static field initializer: no method calls it + static Map build() { + Map m = new HashMap<>(); + m.put("a", "1"); + return m; + } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Engine.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Engine.java new file mode 100644 index 00000000..2dc78506 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Engine.java @@ -0,0 +1,9 @@ +package pkg; + +public final class Engine { + static { + Boot.init(); + } + + public static int run() { return Boot.ready; } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/EngineTest.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/EngineTest.java new file mode 100644 index 00000000..f9b7cf8d --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/EngineTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class EngineTest { + @Test + public void runsAfterBoot() { assert Engine.run() == 1; } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Orphan.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Orphan.java new file mode 100644 index 00000000..0626ac63 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Orphan.java @@ -0,0 +1,8 @@ +package pkg; + +// control: a static field initializer of a type no test touches selects no test +public final class Orphan { + static final int VALUE = Unused.compute(); + + static int value() { return VALUE; } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/OtherTest.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/OtherTest.java new file mode 100644 index 00000000..540b874a --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/OtherTest.java @@ -0,0 +1,9 @@ +package pkg; + +import org.junit.Test; + +// control: touches none of the initialized types +public class OtherTest { + @Test + public void unrelated() { assert new Cache().get().equals("v"); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Registry.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Registry.java new file mode 100644 index 00000000..ff04aee9 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Registry.java @@ -0,0 +1,12 @@ +package pkg; + +import java.util.Map; + +public final class Registry { + // the static initializer runs on the first use of Registry, so a throw in build() fails every user + static final Map TABLE = Defaults.build(); + + private Registry() { } + + public static String find(String key) { return TABLE.get(key); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/RegistryTest.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/RegistryTest.java new file mode 100644 index 00000000..61093871 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/RegistryTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class RegistryTest { + @Test + public void findsKey() { assert "1".equals(Registry.find("a")); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Unused.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Unused.java new file mode 100644 index 00000000..2ef24753 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Unused.java @@ -0,0 +1,5 @@ +package pkg; + +public final class Unused { + static int compute() { return 3; } +} From 34b9ebf8cb009822e3e2f1b7e17018d6f5177635 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:59:28 -0700 Subject: [PATCH 2/4] parser: a field initializer written after a comment is read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `static final F X = // why new F() { ... };` — the Java field extractor took the node right after `=` as the initializer, and a comment is a named node in tree-sitter, so the whole initializer was dropped: its calls, and an anonymous class it creates together with that class's methods. A try-with-resources declaration had the same read. Both now skip comments, as a local variable's initializer already did. The comment-invariance twins gain a field initializer, a field holding an anonymous class and a resource declaration; the case checks that a test reaching the anonymous class's method through its interface is selected, beside the same shape with no comment. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../java/extractors/field-extractor.ts | 5 ++++- .../extractors/local-variable-extractor.ts | 3 ++- .../expressions/CommentsAreInvisible.java | 4 ++++ .../CommentsAreInvisibleControl.java | 4 ++++ .../case.json | 10 ++++++++++ .../src/pkg/Handler.java | 5 +++++ .../src/pkg/Handlers.java | 19 +++++++++++++++++++ .../src/pkg/Router.java | 5 +++++ .../src/pkg/RouterTest.java | 11 +++++++++++ .../src/pkg/Shout.java | 7 +++++++ 10 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/cases/java/field-initializer-after-a-comment/case.json create mode 100644 tests/cases/java/field-initializer-after-a-comment/src/pkg/Handler.java create mode 100644 tests/cases/java/field-initializer-after-a-comment/src/pkg/Handlers.java create mode 100644 tests/cases/java/field-initializer-after-a-comment/src/pkg/Router.java create mode 100644 tests/cases/java/field-initializer-after-a-comment/src/pkg/RouterTest.java create mode 100644 tests/cases/java/field-initializer-after-a-comment/src/pkg/Shout.java diff --git a/parser/src/parsers/java/extractors/field-extractor.ts b/parser/src/parsers/java/extractors/field-extractor.ts index 32dd5af9..f312f5d0 100644 --- a/parser/src/parsers/java/extractors/field-extractor.ts +++ b/parser/src/parsers/java/extractors/field-extractor.ts @@ -470,9 +470,12 @@ export class FieldExtractor { ); // Extract expressions from field initializer (if present) + // Skip comment nodes between = and the initializer, as a local variable's initializer does: + // `static final F X = // why\n new F() { ... };` dropped the whole initializer, anonymous class included const equalsIndex = declarator.children.findIndex(c => c.type === '='); if (equalsIndex >= 0 && equalsIndex < declarator.children.length - 1) { - const initializerNode = declarator.children[equalsIndex + 1]; + const initializerNode = declarator.children.slice(equalsIndex + 1) + .find(c => c.type !== 'line_comment' && c.type !== 'block_comment'); if (initializerNode) { const expressions = this.expressionExtractor.extractFromFieldInitializer( initializerNode, diff --git a/parser/src/parsers/java/extractors/local-variable-extractor.ts b/parser/src/parsers/java/extractors/local-variable-extractor.ts index 90254d5e..53ac9021 100644 --- a/parser/src/parsers/java/extractors/local-variable-extractor.ts +++ b/parser/src/parsers/java/extractors/local-variable-extractor.ts @@ -2525,7 +2525,8 @@ export class LocalVariableExtractor { // Extract initializer expressions const equalsIndex = child.children.findIndex(c => c.type === '='); if (equalsIndex >= 0 && equalsIndex < child.children.length - 1) { - const initializerNode = child.children[equalsIndex + 1]; + const initializerNode = child.children.slice(equalsIndex + 1) + .find(c => c.type !== 'line_comment' && c.type !== 'block_comment'); if (initializerNode) { this.extractInitializerExpressions( initializerNode, diff --git a/parser/src/test-data/java/expressions/CommentsAreInvisible.java b/parser/src/test-data/java/expressions/CommentsAreInvisible.java index 36c85d54..a3a0543d 100644 --- a/parser/src/test-data/java/expressions/CommentsAreInvisible.java +++ b/parser/src/test-data/java/expressions/CommentsAreInvisible.java @@ -39,4 +39,8 @@ public class CommentsAreInvisible { void castExpression() { Object x = (Object) /* a */ f(); } void forClauses() { for (int i = /* a */ 0; /* b */ c; /* c */ i++) { } } int switchArm() { return switch (1) { default -> /* a */ f(); }; } + Object fieldInit = /* a */ new Object(); + Runnable fieldAnon = // a + new Runnable() { public void run() { f(); } }; + void tryResource() throws Exception { try (java.io.StringReader r = /* a */ new java.io.StringReader("")) { f(); } } } diff --git a/parser/src/test-data/java/expressions/CommentsAreInvisibleControl.java b/parser/src/test-data/java/expressions/CommentsAreInvisibleControl.java index d30a7754..f7a61394 100644 --- a/parser/src/test-data/java/expressions/CommentsAreInvisibleControl.java +++ b/parser/src/test-data/java/expressions/CommentsAreInvisibleControl.java @@ -25,4 +25,8 @@ public class CommentsAreInvisibleControl { void castExpression() { Object x = (Object) f(); } void forClauses() { for (int i = 0; c; i++) { } } int switchArm() { return switch (1) { default -> f(); }; } + Object fieldInit = new Object(); + Runnable fieldAnon = + new Runnable() { public void run() { f(); } }; + void tryResource() throws Exception { try (java.io.StringReader r = new java.io.StringReader("")) { f(); } } } diff --git a/tests/cases/java/field-initializer-after-a-comment/case.json b/tests/cases/java/field-initializer-after-a-comment/case.json new file mode 100644 index 00000000..a2c6c6b8 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/case.json @@ -0,0 +1,10 @@ +{"lang": "java", "src": "src", + "checks": [ + {"why": "a field initializer written after a comment is still read: the anonymous class it creates, and what its method calls, are in the graph", + "run": ["impact", "Shout.loud", "--tests"], + "want": ["RouterTest::routesUpper"], + "avoid": ["0 of"]}, + {"why": "control: the same initializer with no comment, read before and after", + "run": ["impact", "Shout.soft", "--tests"], + "want": ["RouterTest::routesLower"], + "avoid": ["0 of"]}]} diff --git a/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handler.java b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handler.java new file mode 100644 index 00000000..129a14b7 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handler.java @@ -0,0 +1,5 @@ +package pkg; + +public interface Handler { + String handle(String s); +} diff --git a/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handlers.java b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handlers.java new file mode 100644 index 00000000..cc5baf28 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Handlers.java @@ -0,0 +1,19 @@ +package pkg; + +public final class Handlers { + private Handlers() { } + + public static final Handler UPPER = + // a comment between the `=` and the initializer, as a formatter or an author leaves one + new Handler() { + @Override + public String handle(String s) { return Shout.loud(s); } + }; + + // control: the same shape with no comment + public static final Handler LOWER = + new Handler() { + @Override + public String handle(String s) { return Shout.soft(s); } + }; +} diff --git a/tests/cases/java/field-initializer-after-a-comment/src/pkg/Router.java b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Router.java new file mode 100644 index 00000000..71e7e0d6 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Router.java @@ -0,0 +1,5 @@ +package pkg; + +public class Router { + public String route(Handler h, String s) { return h.handle(s); } +} diff --git a/tests/cases/java/field-initializer-after-a-comment/src/pkg/RouterTest.java b/tests/cases/java/field-initializer-after-a-comment/src/pkg/RouterTest.java new file mode 100644 index 00000000..a9b06d23 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/src/pkg/RouterTest.java @@ -0,0 +1,11 @@ +package pkg; + +import org.junit.Test; + +public class RouterTest { + @Test + public void routesUpper() { assert "X".equals(new Router().route(Handlers.UPPER, "x")); } + + @Test + public void routesLower() { assert "x".equals(new Router().route(Handlers.LOWER, "X")); } +} diff --git a/tests/cases/java/field-initializer-after-a-comment/src/pkg/Shout.java b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Shout.java new file mode 100644 index 00000000..914a7e27 --- /dev/null +++ b/tests/cases/java/field-initializer-after-a-comment/src/pkg/Shout.java @@ -0,0 +1,7 @@ +package pkg; + +public final class Shout { + static String loud(String s) { return s.toUpperCase(); } + + static String soft(String s) { return s.toLowerCase(); } +} From 7422e84790d3630c1ce5e7bc4d6902d7aec8743b Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:09:46 -0700 Subject: [PATCH 3/4] impact: the at-load hop sees a test that reads a static field A method query is handed the field and field-access facts empty (KIND_SKIPS: only a field or type target was thought to reach them), so the `at load` hop's field clause derived nothing and a test that touches the initialized type only by reading one of its static fields (`Factory.INSTANCE_NULL.lookup(...)`) was dropped. Both relations are now loaded for the kinds that walk the closure; every other rule that joins them also needs a relation that stays skipped for those kinds, so nothing else changes. The case gains that shape. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/axiomcode-impact | 10 +- .../case.json | 101 +++++++++++++++--- .../src/pkg/Rows.java | 11 ++ .../src/pkg/Table.java | 10 ++ .../src/pkg/TableTest.java | 8 ++ 5 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Rows.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Table.java create mode 100644 tests/cases/java/type-initializer-runs-for-its-users/src/pkg/TableTest.java diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index c007571d..645e2e0f 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -129,10 +129,14 @@ _CONFIG = {'config', 'config_key_known', 'config_site'} # exactly the kind that used to be handed them empty. Skipping them there made the rule produce nothing at all, # silently — the one failure mode this table can have. _KEY_LAYER = {'literal', 'dec_literal'} +# and `field` / `faccess` are read by the `at load` hop for every kind that walks the closure: a test that reads a static +# field of a type runs that type's initializer (load_hop / init_use). Handed them empty, a method target's closure that +# reached a field initializer named only the type's CALLERS and dropped every test that touches it through a field. +_LOAD_LAYER = {'field', 'faccess'} KIND_SKIPS = { # relations a query of this kind can never reach - 'method': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER) | _CONFIG, - 'param': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER) | _CONFIG, - 'var': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER) | _CONFIG, + 'method': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER - _LOAD_LAYER) | _CONFIG, + 'param': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER - _LOAD_LAYER) | _CONFIG, + 'var': (_REF_LAYER - {'reexport', 'reexport_from'} - _KEY_LAYER - _LOAD_LAYER) | _CONFIG, 'decoration': (_REF_LAYER - _KEY_LAYER) | _CONFIG, 'config': _REF_LAYER - _KEY_LAYER, 'newconst': (_REF_LAYER - {'switch_over'} - _KEY_LAYER) | _CONFIG, diff --git a/tests/cases/java/type-initializer-runs-for-its-users/case.json b/tests/cases/java/type-initializer-runs-for-its-users/case.json index 211f16bc..86364985 100644 --- a/tests/cases/java/type-initializer-runs-for-its-users/case.json +++ b/tests/cases/java/type-initializer-runs-for-its-users/case.json @@ -1,18 +1,85 @@ -{"lang": "java", "src": "src", +{ + "lang": "java", + "src": "src", "checks": [ - {"why": "a method called only from a static field initializer runs when the type is first used, so a test that calls a static method of that type is selected", - "run": ["impact", "Defaults.build", "--tests"], - "want": ["RegistryTest::findsKey"], - "avoid": ["OtherTest", "ClientTest", "0 of"]}, - {"why": "a method called from an instance field initializer runs in every construction, so a test that builds the type is selected", - "run": ["impact", "Cache.create", "--tests"], - "want": ["ClientTest::readsThroughTheCache"], - "avoid": ["RegistryTest", "EngineTest"]}, - {"why": "a method called from a static block runs when the type is first used", - "run": ["impact", "Boot.init", "--tests"], - "want": ["EngineTest::runsAfterBoot"], - "avoid": ["RegistryTest", "ClientTest"]}, - {"why": "control: the initializer of a type no test uses selects no test", - "run": ["impact", "Unused.compute", "--tests"], - "want": ["0 of"], - "avoid": ["RegistryTest", "ClientTest", "EngineTest", "OtherTest"]}]} + { + "why": "a method called only from a static field initializer runs when the type is first used, so a test that calls a static method of that type is selected", + "run": [ + "impact", + "Defaults.build", + "--tests" + ], + "want": [ + "RegistryTest::findsKey" + ], + "avoid": [ + "OtherTest", + "ClientTest", + "0 of" + ] + }, + { + "why": "a method called from an instance field initializer runs in every construction, so a test that builds the type is selected", + "run": [ + "impact", + "Cache.create", + "--tests" + ], + "want": [ + "ClientTest::readsThroughTheCache" + ], + "avoid": [ + "RegistryTest", + "EngineTest" + ] + }, + { + "why": "a method called from a static block runs when the type is first used", + "run": [ + "impact", + "Boot.init", + "--tests" + ], + "want": [ + "EngineTest::runsAfterBoot" + ], + "avoid": [ + "RegistryTest", + "ClientTest" + ] + }, + { + "why": "a test that only READS a static field of the type runs its initializer too", + "run": [ + "impact", + "Rows.load", + "--tests" + ], + "want": [ + "TableTest::readsTheField" + ], + "avoid": [ + "RegistryTest", + "OtherTest", + "0 of" + ] + }, + { + "why": "control: the initializer of a type no test uses selects no test", + "run": [ + "impact", + "Unused.compute", + "--tests" + ], + "want": [ + "0 of" + ], + "avoid": [ + "RegistryTest", + "ClientTest", + "EngineTest", + "OtherTest" + ] + } + ] +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Rows.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Rows.java new file mode 100644 index 00000000..04a1e7b2 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Rows.java @@ -0,0 +1,11 @@ +package pkg; + +import java.util.Collections; +import java.util.Map; + +public final class Rows { + private Rows() { } + + // runs only from Table's static field initializer + static Map load() { return Collections.singletonMap("k", "v"); } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Table.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Table.java new file mode 100644 index 00000000..c472490d --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/Table.java @@ -0,0 +1,10 @@ +package pkg; + +import java.util.Map; + +public final class Table { + // read directly by the test: reading a static field initializes the type first + public static final Map ROWS = Rows.load(); + + private Table() { } +} diff --git a/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/TableTest.java b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/TableTest.java new file mode 100644 index 00000000..2e94a757 --- /dev/null +++ b/tests/cases/java/type-initializer-runs-for-its-users/src/pkg/TableTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class TableTest { + @Test + public void readsTheField() { assert "v".equals(Table.ROWS.get("k")); } +} From b73381cf4861c8960ba818ca0d9301eb6883ac3e Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:09:47 -0700 Subject: [PATCH 4/4] impact: the members a library runs on a Java object reach whoever builds it `equals`, `hashCode` and `toString` are run by a HashMap key, a StringBuilder, String.format, an assertion; an override of a library method (`iterator` of an Iterable, `size` of a Collection, `close`) by the for-each loop, the collection, try-with-resources. The call is written on a library receiver or not at all, and Object's members are deliberately not fanned to client overrides by the engine, so a change to one had no caller and selected no test. The protocol hop Python already has (whoever constructs the type runs its protocol members) now has Java clauses: a member named equals / hashCode / toString, or one that overrides a library method, is reached by the callers of the type's constructors and of its subtypes' constructors. Still taken only from the change itself, under the `protocol` rung. The SQL fast path declines such a target. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/dl/impact.dl | 11 +++++++++++ .../skills/axiomcode/scripts/graph_sql.py | 14 ++++++++++++++ .../library-runs-the-members-it-is-given/case.json | 14 ++++++++++++++ .../lib/kit/src/kit/Pump.java | 8 ++++++++ .../lib/kit/src/kit/Source.java | 5 +++++ .../src/pkg/Index.java | 10 ++++++++++ .../src/pkg/IndexTest.java | 8 ++++++++ .../src/pkg/Key.java | 14 ++++++++++++++ .../src/pkg/OtherTest.java | 9 +++++++++ .../src/pkg/Tap.java | 8 ++++++++ .../src/pkg/TapTest.java | 9 +++++++++ .../src/pkg/Unseen.java | 7 +++++++ 12 files changed, 117 insertions(+) create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/case.json create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Pump.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Source.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Index.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/IndexTest.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Key.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/OtherTest.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Tap.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/TapTest.java create mode 100644 tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Unseen.java diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index 7ca44f79..be879f51 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -1094,6 +1094,17 @@ direct(q, c, "uses", cat("framework-mediated, not a call: servlet_filter via ", protocol_member(t, m) :- member(t, m, n, _), match("__.*__", n), n != "__init__". .decl constructs(c:symbol, t:symbol) constructs(c, t) :- member(t, i, "__init__", _), edge(c, i, _), c != i. +// THE SAME SHAPE IN JAVA: the members a LIBRARY runs on a client object. `equals`, `hashCode` and `toString` are run +// by a HashMap key, a StringBuilder, String.format, an assertion; an override of a library method (`iterator` of an +// Iterable, `compareTo`, `close`, `size` of a Collection) by the for-each loop, the sort, try-with-resources, the +// collection itself. The call is written on a library receiver or not written at all, and Object's members are not +// fanned to client overrides on purpose (resolution/virtual-dispatch.dl: the CHA blow-up), so a change to one had no +// caller and selected no test. Whoever constructs the type, or a subtype of it, holds the object those calls run on. +// Still only from the CHANGE itself, as above. +protocol_member(t, m) :- member(t, m, n, _), java_decl(m), (n = "equals" ; n = "hashCode" ; n = "toString"). +protocol_member(t, m) :- member(t, m, _, _), java_decl(m), override(b, m), !decl_file(b, _). +constructs(c, t) :- java_decl(t), kind(i, "constructor"), owner(i, t), edge(c, i, _), c != i. +constructs(c, t) :- java_decl(t), extends(s, t), kind(i, "constructor"), owner(i, s), edge(c, i, _), c != i. // The hop is taken only where the CHANGE ITSELF is a protocol member, not wherever one turns up in the closure. // As a global edge it was logically fine and empirically noisy: any closure that happened to contain a protocol // member pulled in every constructor of that type and, through them, their tests. Measured across three subjects diff --git a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py index 1e7d53e7..34521239 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py @@ -3571,6 +3571,17 @@ def _has_framework_hops(q, at=None, site_file=None): return True return False +def _java_protocol(q, mids): + """True when one of these Java methods is a member a library runs on the object (dl/impact.dl protocol_member)""" + if not mids or not _has(q, 'symbols'): return False + ph = ','.join('?' * len(mids)) + if q(f"SELECT 1 FROM symbols WHERE id IN ({ph}) AND file LIKE '%.java' AND name IN ('equals', 'hashCode', 'toString') LIMIT 1", *mids): + return True + return bool(_has(q, 'overrides') and q(f"""SELECT 1 FROM overrides o JOIN methods b ON b.id = o.method_id + JOIN symbols s ON s.id = o.overriding_method_id + WHERE o.overriding_method_id IN ({ph}) AND b.provenance = 'lib' + AND s.file LIKE '%.java' LIMIT 1""", *mids)) + def _reaches_init(q, depth): """True when the closure holds a Java initializer the rules hop on from (load_hop): a type reached as a caller, or a / callable""" @@ -3650,6 +3661,9 @@ def solve_from_targets(q, T, QS, site_file=None, nonsource=(), code=None, at=Non # `target(q,k,s,x)`: a STRING target is written (q,"string","",value) — no symbol at all — so the ids and # the extras are kept apart rather than one standing in for the other. ids = sorted({s_ for _k, s_, _x in mine if s_}) + # a Java member a library runs (dl/impact.dl protocol_member: Object's equals / hashCode / toString, an + # override of a library method) is reached through whoever constructs its type: not ported, so decline + if _java_protocol(q, sorted({s_ for k_, s_, _x in mine if s_ and k_ == 'method'})): return None by_kind, extra = {}, {} for k, s_, x in mine: if s_: by_kind.setdefault(k, set()).add(s_) diff --git a/tests/cases/java/library-runs-the-members-it-is-given/case.json b/tests/cases/java/library-runs-the-members-it-is-given/case.json new file mode 100644 index 00000000..f4cf094f --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/case.json @@ -0,0 +1,14 @@ +{"lang": "java", "src": "src", "library": "lib/kit", + "checks": [ + {"why": "hashCode is run by the map the key is put into; the test that builds the key through Index is selected", + "run": ["impact", "Key.hashCode", "--tests"], + "want": ["IndexTest::countsRepeats"], + "avoid": ["TapTest", "OtherTest", "0 of"]}, + {"why": "an override of a library method is run by the library; the test that builds the object and hands it over is selected", + "run": ["impact", "Tap.next", "--tests"], + "want": ["TapTest::drains"], + "avoid": ["IndexTest", "OtherTest", "0 of"]}, + {"why": "control: a toString on a type no test constructs selects no test", + "run": ["impact", "Unseen.toString", "--tests"], + "want": ["0 of"], + "avoid": ["IndexTest", "TapTest", "OtherTest"]}]} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Pump.java b/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Pump.java new file mode 100644 index 00000000..c034c91a --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Pump.java @@ -0,0 +1,8 @@ +package kit; + +public final class Pump { + private Pump() { } + + // the library calls the client's override: no call to it is written in the client + public static String drain(Source s) { return s.next(); } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Source.java b/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Source.java new file mode 100644 index 00000000..6a92c434 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/lib/kit/src/kit/Source.java @@ -0,0 +1,5 @@ +package kit; + +public interface Source { + String next(); +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Index.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Index.java new file mode 100644 index 00000000..9f726fd4 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Index.java @@ -0,0 +1,10 @@ +package pkg; + +import java.util.HashMap; +import java.util.Map; + +public class Index { + private final Map counts = new HashMap<>(); + + public int put(String name) { return counts.merge(new Key(name), 1, Integer::sum); } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/IndexTest.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/IndexTest.java new file mode 100644 index 00000000..5e328593 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/IndexTest.java @@ -0,0 +1,8 @@ +package pkg; + +import org.junit.Test; + +public class IndexTest { + @Test + public void countsRepeats() { Index i = new Index(); i.put("a"); assert i.put("a") == 2; } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Key.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Key.java new file mode 100644 index 00000000..0d2a3796 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Key.java @@ -0,0 +1,14 @@ +package pkg; + +public final class Key { + private final String name; + + Key(String name) { this.name = name; } + + // run by the HashMap the key is put into: no call is written on a Key receiver + @Override + public int hashCode() { return name.hashCode(); } + + @Override + public boolean equals(Object o) { return o instanceof Key && ((Key) o).name.equals(name); } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/OtherTest.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/OtherTest.java new file mode 100644 index 00000000..ac5cfbd6 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/OtherTest.java @@ -0,0 +1,9 @@ +package pkg; + +import org.junit.Test; + +// control: constructs none of the types above +public class OtherTest { + @Test + public void unrelated() { assert "x".length() == 1; } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Tap.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Tap.java new file mode 100644 index 00000000..becee093 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Tap.java @@ -0,0 +1,8 @@ +package pkg; + +import kit.Source; + +public class Tap implements Source { + @Override + public String next() { return "drip"; } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/TapTest.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/TapTest.java new file mode 100644 index 00000000..28864b46 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/TapTest.java @@ -0,0 +1,9 @@ +package pkg; + +import kit.Pump; +import org.junit.Test; + +public class TapTest { + @Test + public void drains() { assert "drip".equals(Pump.drain(new Tap())); } +} diff --git a/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Unseen.java b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Unseen.java new file mode 100644 index 00000000..de93e179 --- /dev/null +++ b/tests/cases/java/library-runs-the-members-it-is-given/src/pkg/Unseen.java @@ -0,0 +1,7 @@ +package pkg; + +// control: a type no test constructs +public final class Unseen { + @Override + public String toString() { return "unseen"; } +}