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
12 changes: 9 additions & 3 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,16 @@ class Impact:
if tids: out.append(('type', f"{self.g.sym[tids[0]]['kind']} {self.g.disp(tids[0])}" + (f" (+{len(tids)-1})" if len(tids) > 1 else ''), tids))
# a const declared ON the line is the declaration written there: the enclosing module spans the line and a
# function in its initializer (`const h = wrap(async (req, res) => …)`) starts on it, and neither is what was asked
# — unless that function IS the const (`const h = async (req, res) => …` is named `h`), which stays the method
# — unless that function IS the const (`const h = async (req, res) => …` is named `h`), which stays the method.
# A CLASS field is the other way round: G.decl_at_line has already taken it when only its own initializer node
# or lambda is written there, so what is left on its line is a callable of its own that its initializer
# declares (`static cfg = make({ run() {…} })`), and that callable is what the line names (#1752)
if kind is None and out and out[0][0] == 'field' and re.fullmatch(r'.+\.\w+:\d+', s):
named = {self.g.sym[i]['name'] for i in self.methods(s, soft=True) if self.g.sym[i].get('line') == out[0][2][0]['line']}
if not named & {f['name'] for f in out[0][2]}: return out
at = [i for i in self.methods(s, soft=True) if self.g.sym[i].get('line') == out[0][2][0]['line']]
if not {self.g.sym[i]['name'] for i in at} & {f['name'] for f in out[0][2]}:
if at and out[0][2][0].get('kind') == 'field': out = out[1:]
# the same header as every other file:line answer: the line says WHICH declaration of the name
else: return [(k, f"{lbl} (at {s})", p) for k, lbl, p in out]
if kind in (None, 'method'):
mids = self.methods(s, soft=True)
# `Type` written alone is the type, and its construction is Type.<init>, so a bare type name must not
Expand Down
8 changes: 7 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ BODILESS_KINDS = {'METHOD_SIGNATURE', 'TYPE_LITERAL_METHOD_SIGNATURE', 'CALL_SIG
# the one name a front end gives every lambda it declares (Python and C#; Java declares none): a name that says nothing
# about WHICH lambda, so it is never a target on its own (G.lambda_label, G.lambda_target)
LAMBDA_NAMES = {'<lambda>'}
# what a front end synthesises ON a field's line that is the field's own, never a callable written there (G.decl_at_line):
# the node that runs a class's field initializers, and the unnamed function an initializer holds (`cb = wrap(() => …)`),
# which JavaScript and TypeScript name `<arrow>` / `<function-expression>` where Python and C# say `<lambda>`
FIELD_OWNED_NAMES = {'<static-init>', '<instance-init>', '<arrow>', '<function-expression>'}
# a reference the parser binds to a VALUE (refs.entity_kind, as each front end spells it): the name there is a variable,
# a parameter, a member or a function, never a type, so it is not evidence of a type the code uses without declaring
VALUE_BINDINGS = {'VARIABLE', 'LOCAL_VARIABLE', 'GLOBAL_VARIABLE', 'FREE_VARIABLE', 'NONLOCAL_VARIABLE', 'PARAMETER',
Expand Down Expand Up @@ -257,7 +261,9 @@ class G:
fend = max([x.get('end_line') or x['line'] for x in fields], default=ln)
for x in spanning:
s = self.sym.get(x['id']) or self.all_sym.get(x['id']) or {}
if fields and self.is_lambda(x['id']) and ln <= (s.get('line') or 0) and (s.get('end_line') or 0) <= fend: continue
# the initializer's own lambda, or the node that runs the class's field initializers, is the field's
if (fields and (self.is_lambda(x['id']) or s.get('name') in FIELD_OWNED_NAMES)
and ln <= (s.get('line') or 0) and (s.get('end_line') or 0) <= fend): continue
if types and s.get('kind') == 'constructor' and s.get('line') == ln and (s.get('end_line') or ln) == ln: continue
return None # a callable of its own is written on this line
return ('field', fields[0]) if fields else ('type', types[0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
{"why": "and the method of a class expression written in it", "run": ["path", "ClsExpr.<static-init>", "tick"], "want": ["[defines", "<anonymous-class>.m", "tick"]},
{"why": "a one-line instance initializer defines the method of the plain object it holds", "run": ["path", "PlainObj.<instance-init>", "tick"], "want": ["[defines", "onClick", "tick"]},
{"why": "and of the object literal it hands to a call", "run": ["path", "InstObj.<instance-init>", "tick"], "want": ["[defines", "fetch", "tick"]},
{"why": "the line of such an initializer names the callable written inside it, not both", "run": ["impact", "src/app.js:32"], "want": ["change: run (at src/app.js:32)"], "avoid": ["callables at src/app.js:32"]},
{"why": "the line of such an initializer names the callable written inside it, not both", "run": ["impact", "src/app.js:32"], "want": ["change: run (at src/app.js:32)"], "avoid": ["callables at src/app.js:32", "change: field ObjLit.cfg"]},
{"why": "an instance field's initializer that declares a method names that method, as a static one does", "run": ["impact", "src/app.js:47"], "want": ["change: fetch (at src/app.js:47)"], "avoid": ["change: field InstObj.api"]},
{"why": "control: a static field whose initializer only calls is the field, not the node that runs the initializers", "run": ["impact", "src/app.js:6"], "want": ["change: field Registry.count (at src/app.js:6)"], "avoid": ["<static-init>"]},
{"why": "control: an instance field whose initializer only calls is the field", "run": ["impact", "src/app.js:12"], "want": ["change: field Panel.size (at src/app.js:12)"], "avoid": ["<instance-init>"]},
{"why": "control: an arrow in a field's initializer is part of the field", "run": ["impact", "src/app.js:25"], "want": ["change: field Host.cb (at src/app.js:25)"], "avoid": ["change: <arrow>"]},
{"why": "control: on a one-line class the method beside the initializer, and a function after the class, stay its siblings", "run": ["path", "OneLine.<static-init>", "tick"], "want": ["no chain of resolved calls connects"], "avoid": ["defines"], "expect_error": true}]}
4 changes: 2 additions & 2 deletions tests/cases/javascript/wrapped-handler-route/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"avoid": ["reads it"]},
{"why": "a file:line on a const targets the const. It used to resolve to the enclosing <module> (other.js:2) or to the arrow in the initializer (adminController.js:2), so a const could only be asked by its bare name, which answers for every const so named",
"run": ["impact", "src/routes/other.js:2"],
"want": ["const listAdmins [field]", "[in scope] other.<module> src/routes/other.js:3"],
"want": ["const listAdmins (at src/routes/other.js:2) [field]", "[in scope] other.<module> src/routes/other.js:3"],
"avoid": ["other.<module> (at src/routes/other.js:2)", "registered as a GET route"]},
{"why": "and a file that declares its own const of the name reads its own, not the wrapped handler's: other.js's `['root']` is not a reader of the controller's listAdmins",
"run": ["impact", "src/controllers/adminController.js:2"],
Expand Down Expand Up @@ -38,7 +38,7 @@
"avoid": ["src/services/adminService.js"]},
{"why": "a file:line in a file whose name carries a dot (`report.controller.js`) is a file and a line: the dots were read as a package prefix and every such target was refused",
"run": ["impact", "src/controllers/report.controller.js:2"],
"want": ["const listReports [field]"],
"want": ["const listReports (at src/controllers/report.controller.js:2) [field]"],
"avoid": ["matches no package"]},
{"why": "CONTROL: a file:line in a file that is not in the graph is still refused",
"run": ["impact", "src/controllers/nope.controller.js:2"],
Expand Down
Loading