Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed
Original file line number Diff line number Diff line change
Expand Up @@ -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 <module>" or "inside <type>", 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, 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"""
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'}) 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)"""
for a, b, i, k, d, n in decls:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1042,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'
Expand All @@ -1061,7 +1094,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
Expand Down
21 changes: 21 additions & 0 deletions tests/cases/csharp/one-line-statements/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
19 changes: 19 additions & 0 deletions tests/cases/csharp/one-line-statements/new_conf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;

namespace A
{
public class Conf
{
static readonly Dictionary<string, int> Settings = new Dictionary<string, int>
{
["retries"] = 4,
["timeout"] = 10,
};
static readonly int Limit = 1;

public int Get()
{
return Settings["retries"] + Limit;
}
}
}
19 changes: 19 additions & 0 deletions tests/cases/csharp/one-line-statements/src/Conf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;

namespace A
{
public class Conf
{
static readonly Dictionary<string, int> Settings = new Dictionary<string, int>
{
["retries"] = 3,
["timeout"] = 10,
};
static readonly int Limit = 1;

public int Get()
{
return Settings["retries"] + Limit;
}
}
}
15 changes: 15 additions & 0 deletions tests/cases/java/one-line-statements/New_conf.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package a;

import java.util.List;
import java.util.Map;

public class Conf {
static final Map<String, Integer> SETTINGS = Map.of(
"retries", 4,
"timeout", 10);
static final int LIMIT = 1;

int get() {
return SETTINGS.get("retries") + LIMIT;
}
}
21 changes: 21 additions & 0 deletions tests/cases/java/one-line-statements/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
15 changes: 15 additions & 0 deletions tests/cases/java/one-line-statements/src/main/java/a/Conf.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package a;

import java.util.List;
import java.util.Map;

public class Conf {
static final Map<String, Integer> SETTINGS = Map.of(
"retries", 3,
"timeout", 10);
static final int LIMIT = 1;

int get() {
return SETTINGS.get("retries") + LIMIT;
}
}
21 changes: 21 additions & 0 deletions tests/cases/javascript/edits-in-place/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
"<module>",
"LIMIT"
]
}
]
}
8 changes: 8 additions & 0 deletions tests/cases/javascript/edits-in-place/new-conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const Settings = {
retries: 4,
timeoutMs: 1000,
};
export const LIMIT = { max: 1 };
export function total() {
return Settings.retries + LIMIT.max;
}
8 changes: 8 additions & 0 deletions tests/cases/javascript/edits-in-place/src/conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const Settings = {
retries: 3,
timeoutMs: 1000,
};
export const LIMIT = { max: 1 };
export function total() {
return Settings.retries + LIMIT.max;
}
60 changes: 60 additions & 0 deletions tests/cases/python/one-line-statements/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
"<module>",
"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": [
"<classbody>"
]
},
{
"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.<module>"
],
"avoid": [
"count"
]
}
]
}
16 changes: 16 additions & 0 deletions tests/cases/python/one-line-statements/new_conf_cls.py
Original file line number Diff line number Diff line change
@@ -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"]
15 changes: 15 additions & 0 deletions tests/cases/python/one-line-statements/new_conf_mod.py
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 4 additions & 0 deletions tests/cases/python/one-line-statements/new_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
count = 0
print("start")
print("the middle")
count = 5
15 changes: 15 additions & 0 deletions tests/cases/python/one-line-statements/src/conf.py
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 4 additions & 0 deletions tests/cases/python/one-line-statements/src/script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
count = 0
print("start")
print("middle")
count = 5
Loading
Loading