diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 097460996..1c52fef6e 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -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., so a bare type name must not diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index c72f6bca1..98036d052 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -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 = {''} +# 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 `` / `` where Python and C# say `` +FIELD_OWNED_NAMES = {'', '', '', ''} # 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', @@ -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]) diff --git a/tests/cases/javascript/field-initializer-is-a-sibling/case.json b/tests/cases/javascript/field-initializer-is-a-sibling/case.json index 17ac8a0d2..b7c750980 100644 --- a/tests/cases/javascript/field-initializer-is-a-sibling/case.json +++ b/tests/cases/javascript/field-initializer-is-a-sibling/case.json @@ -9,5 +9,9 @@ {"why": "and the method of a class expression written in it", "run": ["path", "ClsExpr.", "tick"], "want": ["[defines", ".m", "tick"]}, {"why": "a one-line instance initializer defines the method of the plain object it holds", "run": ["path", "PlainObj.", "tick"], "want": ["[defines", "onClick", "tick"]}, {"why": "and of the object literal it hands to a call", "run": ["path", "InstObj.", "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": [""]}, + {"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": [""]}, + {"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: "]}, {"why": "control: on a one-line class the method beside the initializer, and a function after the class, stay its siblings", "run": ["path", "OneLine.", "tick"], "want": ["no chain of resolved calls connects"], "avoid": ["defines"], "expect_error": true}]} diff --git a/tests/cases/javascript/wrapped-handler-route/case.json b/tests/cases/javascript/wrapped-handler-route/case.json index e4c85a01f..6b3291313 100644 --- a/tests/cases/javascript/wrapped-handler-route/case.json +++ b/tests/cases/javascript/wrapped-handler-route/case.json @@ -10,7 +10,7 @@ "avoid": ["reads it"]}, {"why": "a file:line on a const targets the const. It used to resolve to the enclosing (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. src/routes/other.js:3"], + "want": ["const listAdmins (at src/routes/other.js:2) [field]", "[in scope] other. src/routes/other.js:3"], "avoid": ["other. (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"], @@ -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"],