From a627e1dde25364ca1f2cf6e01f452438a8537209 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:50:23 -0700 Subject: [PATCH 1/2] changed: charge an edit inside a multi-line field initializer to that field An edit, insertion or comment on a later line of a const or field whose initializer spans several lines was reported as a body change of the module (or 'inside' the type), so test-impact seeded from the whole module. It is now the field's 'initializer changed'. A callable written inside the initializer keeps its own body; a Python module-level name is measured by its statement, not its first-to-last binding span. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../axiomcode/scripts/axiomcode-changed | 31 +++- .../csharp/one-line-statements/case.json | 21 +++ .../csharp/one-line-statements/new_conf.cs | 19 ++ .../csharp/one-line-statements/src/Conf.cs | 19 ++ .../java/one-line-statements/New_conf.java | 15 ++ .../cases/java/one-line-statements/case.json | 21 +++ .../src/main/java/a/Conf.java | 15 ++ .../cases/javascript/edits-in-place/case.json | 21 +++ .../javascript/edits-in-place/new-conf.js | 8 + .../javascript/edits-in-place/src/conf.js | 8 + .../python/one-line-statements/case.json | 60 +++++++ .../one-line-statements/new_conf_cls.py | 16 ++ .../one-line-statements/new_conf_mod.py | 15 ++ .../python/one-line-statements/new_script.py | 4 + .../python/one-line-statements/src/conf.py | 15 ++ .../python/one-line-statements/src/script.py | 4 + .../multi-line-initializer/case.json | 170 ++++++++++++++++++ .../new_call_argument.ts | 24 +++ .../multi-line-initializer/new_comment.ts | 23 +++ .../multi-line-initializer/new_function.ts | 23 +++ .../new_member_added.ts | 24 +++ .../new_member_edited.ts | 23 +++ .../new_member_removed.ts | 22 +++ .../new_method_in_object.ts | 23 +++ .../multi-line-initializer/new_one_line.ts | 23 +++ .../multi-line-initializer/src/config.ts | 23 +++ .../multi-line-initializer/src/use.test.ts | 2 + 27 files changed, 670 insertions(+), 2 deletions(-) create mode 100644 tests/cases/csharp/one-line-statements/new_conf.cs create mode 100644 tests/cases/csharp/one-line-statements/src/Conf.cs create mode 100644 tests/cases/java/one-line-statements/New_conf.java create mode 100644 tests/cases/java/one-line-statements/src/main/java/a/Conf.java create mode 100644 tests/cases/javascript/edits-in-place/new-conf.js create mode 100644 tests/cases/javascript/edits-in-place/src/conf.js create mode 100644 tests/cases/python/one-line-statements/new_conf_cls.py create mode 100644 tests/cases/python/one-line-statements/new_conf_mod.py create mode 100644 tests/cases/python/one-line-statements/new_script.py create mode 100644 tests/cases/python/one-line-statements/src/conf.py create mode 100644 tests/cases/python/one-line-statements/src/script.py create mode 100644 tests/cases/typescript/multi-line-initializer/case.json create mode 100644 tests/cases/typescript/multi-line-initializer/new_call_argument.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_comment.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_function.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_member_added.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_member_edited.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_member_removed.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_method_in_object.ts create mode 100644 tests/cases/typescript/multi-line-initializer/new_one_line.ts create mode 100644 tests/cases/typescript/multi-line-initializer/src/config.ts create mode 100644 tests/cases/typescript/multi-line-initializer/src/use.test.ts diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed index 03c0b597..0bb793e9 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed @@ -769,6 +769,29 @@ class Changed: if not lam or (m and m[3] != 'module' and m[0] <= lam[0] and lam[1] <= m[1]): return None if any(k in ('field', 'const', 'enum_member', 'variable') and a <= lam[0] <= max(b, a) for a, b, i, k, d, n in decls): return None return lam + FIELD_KINDS = ('field', 'const', 'enum_member', 'variable') + def field_spanning(ln, first=False): + """A FIELD'S INITIALIZER IS THE FIELD'S, however many lines it spans. Only a field's first line was charged to + it, so an edit to the second line of `export const A = {\n x: 1,\n};` (a schema object, a token table, a + multi-line static initializer) was "body " or "inside ", and test-impact seeded from every + declaration of the module instead of the const. The narrowest field whose span holds ln after its first line + (from it too with first=True, for an insertion after that line); None when a callable written inside the + initializer (an object literal's method) holds ln itself: that callable's body is its own. + A Python module-level name is recorded from its first binding to its last (`x = 0` at the top, `x = 5` at the + bottom spans the file), not by its statement: there the span is the statement's own lines, while a bracket + it opens is still open or a line ends in a backslash""" + def end(x): + if not is_py: return x[1] + depth, j = 0, x[0] + while j <= len(OS): + t = OS[j - 1]; depth += sum(t.count(c) for c in '([{') - sum(t.count(c) for c in ')]}') + if depth <= 0 and not t.rstrip().endswith('\\'): return j + j += 1 + return x[0] + inside = lambda x: x[0] <= ln < end(x) if first else x[0] < ln <= end(x) + fx = min((x for x in decls if x[3] in FIELD_KINDS and inside(x)), key=lambda x: end(x) - x[0], default=None) + inner = narrowest(ln, {'method', 'function', 'constructor'}) if fx else None + return None if inner and fx[0] <= inner[0] and inner[1] <= end(fx) else fx def decl_after(ln, L): """the declaration whose header starts within 3 lines after ln (an annotation / decorator line belongs to it)""" for a, b, i, k, d, n in decls: @@ -785,6 +808,10 @@ class Changed: m = narrowest(after, {'method', 'function', 'constructor'}) if after else None if m and m[0] <= after < self.header_end(OL, m[0]): hits.setdefault(('signature', m), set()).add(after); header_ins.add((after, j1, j2)) + # and a line inserted into a field's initializer (a member added to a multi-line object) is that field's + for ins in list(added): + fx = field_spanning(ins[0], first=True) if ins[0] and ins not in header_ins else None + if fx: hits.setdefault(('field', fx), set()).add(ins[0]); added.remove(ins) def counterpart(ln): """the new line an old changed line became: the most similar line of its replaced block; None when deleted""" if ln in removed_lines: return None @@ -816,7 +843,7 @@ class Changed: f = None # a lambda's lines inside a field's initializer are the field's elif not f: f = next(((a, b, i, k, d, n) for a, b, i, k, d, n in decls if k in ('field', 'const', 'enum_member', 'variable') and a < ln - and any(is_lam(x) and x[0] <= ln <= x[1] and a <= x[0] <= max(b, a) for x in decls)), None) + and any(is_lam(x) and x[0] <= ln <= x[1] and a <= x[0] <= max(b, a) for x in decls)), None) or field_spanning(ln) m = narrowest(ln, {'method', 'function', 'constructor', 'module'}) # A FUNCTION WRITTEN IN ANOTHER'S PARAMETER LIST IS PART OF THAT HEADER. A default value # (`clock = { millis: () => Date.now() }`) holds a callable the graph records on its own; an edit inside the @@ -1061,7 +1088,7 @@ class Changed: # holds it. Never a signature, never nothing; a comment in a module's top level belongs to no declaration for ln in sorted(comment_only): x = next(((a, b, i, k, d, n) for a, b, i, k, d, n in decls if a == ln and k in ('field', 'const', 'enum_member', 'variable')), None) or \ - next((m for m in [narrowest(ln, {'method', 'function', 'constructor'})] if m), None) or \ + field_spanning(ln) or next((m for m in [narrowest(ln, {'method', 'function', 'constructor'})] if m), None) or \ narrowest(ln, {'class', 'interface', 'enum', 'type'}) if not x or any(e.get('id') == x[2] for e in out): continue a, b, i, k, d, n = x diff --git a/tests/cases/csharp/one-line-statements/case.json b/tests/cases/csharp/one-line-statements/case.json index d363cc13..6dad10ce 100644 --- a/tests/cases/csharp/one-line-statements/case.json +++ b/tests/cases/csharp/one-line-statements/case.json @@ -81,6 +81,27 @@ "avoid": [ " field " ] + }, + { + "why": "an edit inside a static readonly initializer written over several lines (a collection initializer) is that field's, not 'inside Conf'", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/Conf.cs", + "--new", + "{repo}/new_conf.cs", + "--file", + "src/Conf.cs" + ], + "want": [ + "field Conf.Settings", + "initializer" + ], + "avoid": [ + "inside Conf", + "Conf.Limit" + ] } ] } \ No newline at end of file diff --git a/tests/cases/csharp/one-line-statements/new_conf.cs b/tests/cases/csharp/one-line-statements/new_conf.cs new file mode 100644 index 00000000..f4b0f8ad --- /dev/null +++ b/tests/cases/csharp/one-line-statements/new_conf.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace A +{ + public class Conf + { + static readonly Dictionary Settings = new Dictionary + { + ["retries"] = 4, + ["timeout"] = 10, + }; + static readonly int Limit = 1; + + public int Get() + { + return Settings["retries"] + Limit; + } + } +} diff --git a/tests/cases/csharp/one-line-statements/src/Conf.cs b/tests/cases/csharp/one-line-statements/src/Conf.cs new file mode 100644 index 00000000..8f11af76 --- /dev/null +++ b/tests/cases/csharp/one-line-statements/src/Conf.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace A +{ + public class Conf + { + static readonly Dictionary Settings = new Dictionary + { + ["retries"] = 3, + ["timeout"] = 10, + }; + static readonly int Limit = 1; + + public int Get() + { + return Settings["retries"] + Limit; + } + } +} diff --git a/tests/cases/java/one-line-statements/New_conf.java b/tests/cases/java/one-line-statements/New_conf.java new file mode 100644 index 00000000..ed99655a --- /dev/null +++ b/tests/cases/java/one-line-statements/New_conf.java @@ -0,0 +1,15 @@ +package a; + +import java.util.List; +import java.util.Map; + +public class Conf { + static final Map SETTINGS = Map.of( + "retries", 4, + "timeout", 10); + static final int LIMIT = 1; + + int get() { + return SETTINGS.get("retries") + LIMIT; + } +} diff --git a/tests/cases/java/one-line-statements/case.json b/tests/cases/java/one-line-statements/case.json index a021ad92..991a9f99 100644 --- a/tests/cases/java/one-line-statements/case.json +++ b/tests/cases/java/one-line-statements/case.json @@ -143,6 +143,27 @@ "Box.lo", "added Box.high" ] + }, + { + "why": "an edit on a later line of a static final initializer written over several lines is that field's, not 'inside Conf'", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/main/java/a/Conf.java", + "--new", + "{repo}/New_conf.java", + "--file", + "src/main/java/a/Conf.java" + ], + "want": [ + "field Conf.SETTINGS", + "initializer" + ], + "avoid": [ + "inside Conf", + "Conf.LIMIT" + ] } ] } \ No newline at end of file diff --git a/tests/cases/java/one-line-statements/src/main/java/a/Conf.java b/tests/cases/java/one-line-statements/src/main/java/a/Conf.java new file mode 100644 index 00000000..214da4cd --- /dev/null +++ b/tests/cases/java/one-line-statements/src/main/java/a/Conf.java @@ -0,0 +1,15 @@ +package a; + +import java.util.List; +import java.util.Map; + +public class Conf { + static final Map SETTINGS = Map.of( + "retries", 3, + "timeout", 10); + static final int LIMIT = 1; + + int get() { + return SETTINGS.get("retries") + LIMIT; + } +} diff --git a/tests/cases/javascript/edits-in-place/case.json b/tests/cases/javascript/edits-in-place/case.json index b595e6e8..8b6ce905 100644 --- a/tests/cases/javascript/edits-in-place/case.json +++ b/tests/cases/javascript/edits-in-place/case.json @@ -200,6 +200,27 @@ "signature ", "removed " ] + }, + { + "why": "an edit on a later line of a const whose object initializer spans lines is that const's, not the module's body", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/conf.js", + "--new", + "{repo}/new-conf.js", + "--file", + "src/conf.js" + ], + "want": [ + "field Settings", + "initializer" + ], + "avoid": [ + "", + "LIMIT" + ] } ] } \ No newline at end of file diff --git a/tests/cases/javascript/edits-in-place/new-conf.js b/tests/cases/javascript/edits-in-place/new-conf.js new file mode 100644 index 00000000..6c9bd63b --- /dev/null +++ b/tests/cases/javascript/edits-in-place/new-conf.js @@ -0,0 +1,8 @@ +export const Settings = { + retries: 4, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export function total() { + return Settings.retries + LIMIT.max; +} diff --git a/tests/cases/javascript/edits-in-place/src/conf.js b/tests/cases/javascript/edits-in-place/src/conf.js new file mode 100644 index 00000000..63bae002 --- /dev/null +++ b/tests/cases/javascript/edits-in-place/src/conf.js @@ -0,0 +1,8 @@ +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export function total() { + return Settings.retries + LIMIT.max; +} diff --git a/tests/cases/python/one-line-statements/case.json b/tests/cases/python/one-line-statements/case.json index 678bb2dc..a2052746 100644 --- a/tests/cases/python/one-line-statements/case.json +++ b/tests/cases/python/one-line-statements/case.json @@ -167,6 +167,66 @@ "variable lo", "variable j" ] + }, + { + "why": "an edit inside a module-level dict written over several lines is that name's, not the module's body", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/conf.py", + "--new", + "{repo}/new_conf_mod.py", + "--file", + "src/conf.py" + ], + "want": [ + "field SETTINGS", + "initializer" + ], + "avoid": [ + "", + "LIMIT" + ] + }, + { + "why": "and a list item added to a class attribute written over several lines is that attribute's, not the class body's", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/conf.py", + "--new", + "{repo}/new_conf_cls.py", + "--file", + "src/conf.py" + ], + "want": [ + "field Store.ROUTES", + "initializer" + ], + "avoid": [ + "" + ] + }, + { + "why": "control: a statement between two bindings of a module-level name is not that name's (its recorded span runs from the first binding to the last)", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/script.py", + "--new", + "{repo}/new_script.py", + "--file", + "src/script.py" + ], + "want": [ + "body script." + ], + "avoid": [ + "count" + ] } ] } \ No newline at end of file diff --git a/tests/cases/python/one-line-statements/new_conf_cls.py b/tests/cases/python/one-line-statements/new_conf_cls.py new file mode 100644 index 00000000..cdf0f602 --- /dev/null +++ b/tests/cases/python/one-line-statements/new_conf_cls.py @@ -0,0 +1,16 @@ +SETTINGS = { + "retries": 3, + "timeout": 10, +} +LIMIT = {"max": 1} + + +class Store: + ROUTES = [ + "a", + "b", + "c", + ] + + def get(self): + return SETTINGS["retries"] + LIMIT["max"] diff --git a/tests/cases/python/one-line-statements/new_conf_mod.py b/tests/cases/python/one-line-statements/new_conf_mod.py new file mode 100644 index 00000000..9273a097 --- /dev/null +++ b/tests/cases/python/one-line-statements/new_conf_mod.py @@ -0,0 +1,15 @@ +SETTINGS = { + "retries": 4, + "timeout": 10, +} +LIMIT = {"max": 1} + + +class Store: + ROUTES = [ + "a", + "b", + ] + + def get(self): + return SETTINGS["retries"] + LIMIT["max"] diff --git a/tests/cases/python/one-line-statements/new_script.py b/tests/cases/python/one-line-statements/new_script.py new file mode 100644 index 00000000..da6e1e13 --- /dev/null +++ b/tests/cases/python/one-line-statements/new_script.py @@ -0,0 +1,4 @@ +count = 0 +print("start") +print("the middle") +count = 5 diff --git a/tests/cases/python/one-line-statements/src/conf.py b/tests/cases/python/one-line-statements/src/conf.py new file mode 100644 index 00000000..315fe390 --- /dev/null +++ b/tests/cases/python/one-line-statements/src/conf.py @@ -0,0 +1,15 @@ +SETTINGS = { + "retries": 3, + "timeout": 10, +} +LIMIT = {"max": 1} + + +class Store: + ROUTES = [ + "a", + "b", + ] + + def get(self): + return SETTINGS["retries"] + LIMIT["max"] diff --git a/tests/cases/python/one-line-statements/src/script.py b/tests/cases/python/one-line-statements/src/script.py new file mode 100644 index 00000000..7a5620c2 --- /dev/null +++ b/tests/cases/python/one-line-statements/src/script.py @@ -0,0 +1,4 @@ +count = 0 +print("start") +print("middle") +count = 5 diff --git a/tests/cases/typescript/multi-line-initializer/case.json b/tests/cases/typescript/multi-line-initializer/case.json new file mode 100644 index 00000000..276bacd7 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/case.json @@ -0,0 +1,170 @@ +{ + "lang": "typescript", + "src": "src", + "checks": [ + { + "why": "an edit on the second line of a const whose object initializer spans lines is that const's initializer, not the module's body", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_member_edited.ts", + "--file", + "src/config.ts" + ], + "want": [ + "field Settings", + "initializer" + ], + "avoid": [ + "" + ] + }, + { + "why": "a member inserted into a multi-line `as const` object is that const's change", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_member_added.ts", + "--file", + "src/config.ts" + ], + "want": [ + "field Tokens", + "initializer" + ], + "avoid": [ + "", + "new field" + ] + }, + { + "why": "a member removed from it too", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_member_removed.ts", + "--file", + "src/config.ts" + ], + "want": [ + "field Tokens", + "initializer" + ], + "avoid": [ + "", + "removed" + ] + }, + { + "why": "a member added to an object handed to a call (a schema builder) is the const the call initializes", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_call_argument.ts", + "--file", + "src/config.ts" + ], + "want": [ + "field OrderShape", + "initializer" + ], + "avoid": [ + "" + ] + }, + { + "why": "a comment added inside the initializer is that const's, as a comment only", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_comment.ts", + "--file", + "src/config.ts" + ], + "want": [ + "OrderShape", + "comment only" + ], + "avoid": [ + "" + ] + }, + { + "why": "control: a one-line const is named as before", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_one_line.ts", + "--file", + "src/config.ts" + ], + "want": [ + "field LIMIT", + "initializer" + ], + "avoid": [ + "", + "Settings" + ] + }, + { + "why": "control: a method written in an object literal keeps its own body", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_method_in_object.ts", + "--file", + "src/config.ts" + ], + "want": [ + "body run src/config.ts:17" + ], + "avoid": [ + "", + "field api" + ] + }, + { + "why": "control: a function below the consts is its own body, not a const above it", + "run": [ + "changed", + "{repo}", + "--old", + "{repo}/src/config.ts", + "--new", + "{repo}/new_function.ts", + "--file", + "src/config.ts" + ], + "want": [ + "body total" + ], + "avoid": [ + "", + "field Settings", + "field LIMIT" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/typescript/multi-line-initializer/new_call_argument.ts b/tests/cases/typescript/multi-line-initializer/new_call_argument.ts new file mode 100644 index 00000000..40b7f1c8 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_call_argument.ts @@ -0,0 +1,24 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', + channel: 'string', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_comment.ts b/tests/cases/typescript/multi-line-initializer/new_comment.ts new file mode 100644 index 00000000..fb659a71 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_comment.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', // the order's key + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_function.ts b/tests/cases/typescript/multi-line-initializer/new_function.ts new file mode 100644 index 00000000..6433f153 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_function.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs - LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_member_added.ts b/tests/cases/typescript/multi-line-initializer/new_member_added.ts new file mode 100644 index 00000000..7281b874 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_member_added.ts @@ -0,0 +1,24 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', + Cache: 'Cache', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_member_edited.ts b/tests/cases/typescript/multi-line-initializer/new_member_edited.ts new file mode 100644 index 00000000..c724701d --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_member_edited.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 4, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_member_removed.ts b/tests/cases/typescript/multi-line-initializer/new_member_removed.ts new file mode 100644 index 00000000..d52d9352 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_member_removed.ts @@ -0,0 +1,22 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_method_in_object.ts b/tests/cases/typescript/multi-line-initializer/new_method_in_object.ts new file mode 100644 index 00000000..cced1ac6 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_method_in_object.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries + 1; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/new_one_line.ts b/tests/cases/typescript/multi-line-initializer/new_one_line.ts new file mode 100644 index 00000000..db8e6479 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/new_one_line.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 2 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/src/config.ts b/tests/cases/typescript/multi-line-initializer/src/config.ts new file mode 100644 index 00000000..a574aedd --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/src/config.ts @@ -0,0 +1,23 @@ +function shape(fields: T): T { return fields; } + +export const Settings = { + retries: 3, + timeoutMs: 1000, +}; +export const LIMIT = { max: 1 }; +export const Tokens = { + Clock: 'Clock', + Store: 'Store', +} as const; +export const OrderShape = shape({ + id: 'string', + total: 'number', +}); +export const api = { + run(): number { + return Settings.retries; + }, +}; +export function total(): number { + return Settings.timeoutMs + LIMIT.max; +} diff --git a/tests/cases/typescript/multi-line-initializer/src/use.test.ts b/tests/cases/typescript/multi-line-initializer/src/use.test.ts new file mode 100644 index 00000000..25d6e778 --- /dev/null +++ b/tests/cases/typescript/multi-line-initializer/src/use.test.ts @@ -0,0 +1,2 @@ +import { total, Tokens, OrderShape } from './config'; +test('total', () => { expect(total()).toBe(1001); expect(Tokens.Clock).toBe('Clock'); expect(OrderShape.id).toBe('string'); }); From d2afa19d755abfe77f91c7559a89d409c7a26b86 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:02:54 -0700 Subject: [PATCH 2/2] changed: a const object's data key edit is its initializer; a table's lambda keeps its body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since data keys of const objects are declarations, an edit to a key's value was read as a typed field: 'type 3, → 4,', and on a one-line const 'LIMIT.max renamed → LIMIT'. It is now the key's 'initializer changed'. A lambda on a later line of a multi-line initializer is its own body again, as a method written there already was. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../axiomcode/scripts/axiomcode-changed | 10 +++++-- .../multi-line-initializer/case.json | 28 +++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed index 0bb793e9..2800a20c 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed @@ -776,7 +776,7 @@ class Changed: multi-line static initializer) was "body " or "inside ", and test-impact seeded from every declaration of the module instead of the const. The narrowest field whose span holds ln after its first line (from it too with first=True, for an insertion after that line); None when a callable written inside the - initializer (an object literal's method) holds ln itself: that callable's body is its own. + initializer (an object literal's method, a table's lambda) holds ln itself: that callable's body is its own. A Python module-level name is recorded from its first binding to its last (`x = 0` at the top, `x = 5` at the bottom spans the file), not by its statement: there the span is the statement's own lines, while a bracket it opens is still open or a line ends in a backslash""" @@ -790,7 +790,7 @@ class Changed: return x[0] inside = lambda x: x[0] <= ln < end(x) if first else x[0] < ln <= end(x) fx = min((x for x in decls if x[3] in FIELD_KINDS and inside(x)), key=lambda x: end(x) - x[0], default=None) - inner = narrowest(ln, {'method', 'function', 'constructor'}) if fx else None + inner = (narrowest(ln, {'method', 'function', 'constructor'}) or narrowest(ln, None, lambdas=True)) if fx else None return None if inner and fx[0] <= inner[0] and inner[1] <= end(fx) else fx def decl_after(ln, L): """the declaration whose header starts within 3 lines after ln (an annotation / decorator line belongs to it)""" @@ -1069,6 +1069,12 @@ class Changed: detail = (f'may have changed: still assigned at line {again}' if again else f'may have changed: no assignment of it near its line, but the new text still names it' if any(re.search(rf'\b{re.escape(n)}\b', x) for x in NS) else 'removed or renamed') + # A DATA KEY OF A CONST OBJECT (`Settings.retries` in `const Settings = { retries: 3 }`) is written `key: value`: + # read as a typed field, `retries: 3` → `retries: 4` was "type 3, → 4,", and on a one-line const the key + # line's declared name (`LIMIT`) made `LIMIT.max` "renamed → LIMIT". Its value is its initializer + elif '.' in d and any(kk in FIELD_KINDS and dd == d.split('.')[0] for aa, bb, ii, kk, dd, nn in decls): + detail = ('initializer changed' if re.search(rf'(?:^|[{{,\s])["\']?{re.escape(n)}["\']?\s*:', nline) else + 'removed or renamed') elif ot and nt and ot != nt: detail = f'type {ot} → {nt}' elif nn_ and nn_ != n: detail = f'renamed → {nn_}' else: detail = 'initializer / modifiers changed' diff --git a/tests/cases/typescript/multi-line-initializer/case.json b/tests/cases/typescript/multi-line-initializer/case.json index 276bacd7..88ae0ed5 100644 --- a/tests/cases/typescript/multi-line-initializer/case.json +++ b/tests/cases/typescript/multi-line-initializer/case.json @@ -3,7 +3,7 @@ "src": "src", "checks": [ { - "why": "an edit on the second line of a const whose object initializer spans lines is that const's initializer, not the module's body", + "why": "an edit on the second line of a const whose object initializer spans lines is that const's (here its declared key's) initializer, not the module's body, and a key's value is no type", "run": [ "changed", "{repo}", @@ -15,11 +15,12 @@ "src/config.ts" ], "want": [ - "field Settings", - "initializer" + "field Settings.retries", + "initializer changed" ], "avoid": [ - "" + "", + "type 3" ] }, { @@ -44,7 +45,7 @@ ] }, { - "why": "a member removed from it too", + "why": "a member removed from it is that key's removal, not the module's body", "run": [ "changed", "{repo}", @@ -56,12 +57,10 @@ "src/config.ts" ], "want": [ - "field Tokens", - "initializer" + "removed Tokens.Store" ], "avoid": [ - "", - "removed" + "" ] }, { @@ -105,7 +104,7 @@ ] }, { - "why": "control: a one-line const is named as before", + "why": "control: an edit to a one-line const's value is still its initializer, never a rename of its key to the const's name", "run": [ "changed", "{repo}", @@ -117,12 +116,13 @@ "src/config.ts" ], "want": [ - "field LIMIT", - "initializer" + "LIMIT", + "initializer changed" ], "avoid": [ "", - "Settings" + "Settings", + "renamed" ] }, { @@ -167,4 +167,4 @@ ] } ] -} \ No newline at end of file +}