diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index index 962c0b4c4..9130f20dc 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index @@ -20,8 +20,9 @@ come from methods/types alone, refs/literals/comments are empty, and index_meta """ import csv, os, re, sqlite3, sys, time, glob, collections csv.field_size_limit(10**9) -INDEX_VERSION = '5' # bump when the tables' CONTENT changes shape (v2: JavaScript arrows named after their variable; v3: paths table, sites view, no variable row for a bound function; - # v5: JavaScript fields owned by their class, computed-key members named by their key, anonymous class expressions named by their binding); the query +INDEX_VERSION = '6' # bump when the tables' CONTENT changes shape (v2: JavaScript arrows named after their variable; v3: paths table, sites view, no variable row for a bound function; + # v5: JavaScript fields owned by their class, computed-key members named by their key, anonymous class expressions named by their binding; + # v6: TypeScript class-property arrows named after their field); the query # frontend re-indexes an older graph when the IR is still there REPO = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 and not sys.argv[1].startswith('-') else os.environ.get('AXIOMCODE_REPO') or '.') @@ -98,8 +99,13 @@ A = { kind=lambda r: 'const' if r.get('isConst') == 'true' else 'variable', name='name', owner=None, filePath='filePath', line='startLine', end='endLine'), dict(file='all-typescript-fields.csv', id='tsFieldUniqueHash', kind=lambda r: 'field', name='name', owner='ownerQualifiedName', filePath='filePath', line='startLine', end='endLine'), dict(file='all-typescript-enum-members.csv', kind=lambda r: 'enum_member', name='name', owner='ownerQualifiedName', filePath='filePath', line='startLine', end='endLine')], - # `const foo = () => …` : the method is named ; its name is the variable's - boundNames=dict(file='all-typescript-variables.csv', name='name', method='boundFunctionLinkHash'), + # `const foo = () => …` : the method is named ; its name is the variable's. A class property `handler = () => …` + # is the same shape through the field: its initializer is the ARROW_FUNCTION row whose anonymousDeclarationHash is the + # method, so `this.handler()` (resolved to that method) is a caller of `Svc.handler`, not of `Svc.`. + # `handler = debounce(() => …)` is not: its initializer is the CALL, which declares nothing + boundNames=[dict(file='all-typescript-variables.csv', name='name', method='boundFunctionLinkHash'), + dict(file='all-typescript-fields.csv', name='name', + via=dict(key='initializerExpressionLinkHash', file='all-typescript-expressions.csv', id='tsExpressionUniqueHash', method='anonymousDeclarationHash'))], skipped='skipped-typescript-files.csv'), 'python': dict( modules=dict(file='all-python-modules.csv', id='pyModuleUniqueHash', filePath='filePath'), @@ -224,19 +230,25 @@ c.executemany("INSERT OR IGNORE INTO paths VALUES (?,?)", ((p, rel(p)) for p in # ── names a function or class takes from what it is bound to ────────────────────────────────────── bound = {} -if A['boundNames']: - b = A['boundNames'] - if b.get('via'): # variable -> expression -> method +# method -> (file, line, name) of the variable or field that names it, where that row says its own place: `run:\n T =\n (…) => …` +# starts the field lines above its arrow, and without this the field stayed a second `Holder.run`, a field beside the method +bound_src = {} +def bind(m_, r, name): + if m_ in bound: return + bound[m_] = name + if r.get('filePath') and (r.get('startLine') or '').isdigit(): bound_src[m_] = (rel(r['filePath']), int(r['startLine']), name) +for b in ([A['boundNames']] if isinstance(A['boundNames'], dict) else A['boundNames'] or []): + if b.get('via'): # variable (or field) -> expression -> method v = b['via']; expr2m = {} for r in rows(v['file']): if r.get(v['method']) and r.get(v['id']): expr2m[r[v['id']]] = r[v['method']] for r in rows(b['file']): if b.get('only') and not b['only'](r): continue m_ = expr2m.get(r.get(v['key'], '')) - if m_ and r.get(b['name']): bound.setdefault(m_, r[b['name']]) + if m_ and r.get(b['name']): bind(m_, r, r[b['name']]) else: for r in rows(b['file']): - if r.get(b['method']): bound.setdefault(r[b['method']], r[b['name']]) + if r.get(b['method']): bind(r[b['method']], r, r[b['name']]) # a function that is the value of a property takes the property's name: `recv.key = function …` (an ASSIGNMENT whose # target is a non-computed property access) and `{ key: () => … }` (an OBJECT_LITERAL's PROPERTY_VALUE, keyed by the # PROPERTY_KEY sibling at the same childIndex). The owner is the receiver as written (`helpers`, `Foo` for @@ -363,7 +375,9 @@ def enum_constant_of(fp, ln): return None for m in c.execute("SELECT id, name, qualified_name, signature, kind, owner_type_id, owner_qualified_name, file_path, start_line, end_line FROM methods WHERE provenance='client'"): name = m['name'] or ckey.get(cmeth.get(m['id'], ''), '') - if name.startswith('<') and m['id'] in bound: name = bound[m['id']]; bound_at.add((rel(m['file_path']), m['start_line'], name)) + if name.startswith('<') and m['id'] in bound: + name = bound[m['id']]; bound_at.add((rel(m['file_path']), m['start_line'], name)) + if m['id'] in bound_src: bound_at.add(bound_src[m['id']]) od = tdisplay(m['owner_type_id']) if m['owner_type_id'] else bound_owner.get(m['id']) if m['kind'] == 'ENUM_CONSTANT_METHOD' and od: k_ = enum_constant_of(rel(m['file_path']), m['start_line'] or 0) diff --git a/tests/cases/typescript/class-property-arrow/case.json b/tests/cases/typescript/class-property-arrow/case.json new file mode 100644 index 000000000..ef1ce35bc --- /dev/null +++ b/tests/cases/typescript/class-property-arrow/case.json @@ -0,0 +1,23 @@ +{"lang": "typescript", "src": "src", + "checks": [ + {"why": "a class property initialised with an arrow is a method named for its field: the engine resolves this.handler() to that arrow, so the caller is resolved, not a same-scope guess, and the member reads as Svc.handler, never Svc.", + "run": ["impact", "Svc.handler"], + "want": ["Svc.run", "[resolved]"], + "avoid": ["Svc. src/svc.ts:6", "nothing named", "[in scope] Svc.run"]}, + {"why": "a property written over several lines (run:\n T =\n () => ...) starts its field lines above its arrow: it is still one member, the method, not a field beside a method of the same name", + "run": ["impact", "Svc.wide"], + "want": ["change: Svc.wide [method]", "Svc.go"], + "avoid": ["more than one kind", "field Svc.wide"]}, + {"why": "the arrow's own callee lists it by its field's name", + "run": ["impact", "audit"], + "want": ["Svc.handler", "standalone"], + "avoid": ["Svc. src/svc.ts:6"]}, + {"why": "control: a property whose initializer is a CALL wrapping the arrow (debounce(() => ...)) stays a field; the wrapped arrow does not take its name", + "run": ["impact", "Svc.throttled"], + "want": ["change: field Svc.throttled"], + "avoid": ["nothing named"]}, + {"why": "control: a module const bound to an arrow is still the function standalone", + "run": ["impact", "standalone"], + "want": ["main"], + "avoid": ["", "nothing named"]} + ]} diff --git a/tests/cases/typescript/class-property-arrow/src/main.ts b/tests/cases/typescript/class-property-arrow/src/main.ts new file mode 100644 index 000000000..4d89933fc --- /dev/null +++ b/tests/cases/typescript/class-property-arrow/src/main.ts @@ -0,0 +1,6 @@ +import { Svc, standalone } from './svc'; + +export function main(): number { + standalone(); + return new Svc().run(); +} diff --git a/tests/cases/typescript/class-property-arrow/src/svc.ts b/tests/cases/typescript/class-property-arrow/src/svc.ts new file mode 100644 index 000000000..70e5d269a --- /dev/null +++ b/tests/cases/typescript/class-property-arrow/src/svc.ts @@ -0,0 +1,22 @@ +declare function debounce any>(fn: T): T; + +export function audit(): number { return 1; } + +export class Svc { + handler = () => { return audit(); }; + throttled = debounce(() => audit()); + label = 'svc'; + + run(): number { return this.handler(); } + later(): number { return this.throttled(); } + name(): string { return this.label; } + + wide: + Fn = + () => audit(); + go(): number { return this.wide(); } +} + +export type Fn = () => number; + +export const standalone = () => audit();