Skip to content

Commit 7f3b703

Browse files
swapnilpaliwal-sdaxiomcode-bot[bot]claude
committed
impact: a private class field (Owner.#name) resolves under its # spelling
The resolver rewrote '#' to '.' for everything but methods, so Vault.#secret became Vault..secret and #secret became secret: no spelling reached the field. A '#' name is now looked up verbatim as a field before the rewrite. JavaScript class fields were also indexed with no owner, so even a public field's readers were only 'alongside' rows; the field now takes its class from ownerTypeLinkHash (and its own id), as TypeScript's already do. Golden cases for JS and TS, with controls: a private method, a public field, and a same-named private field in another class. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cb075fe commit 7f3b703

6 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ class Impact:
427427
if '#' in verb:
428428
hit = self.g.q("SELECT id FROM symbols WHERE (display = ? OR qualified_name = ?) AND method_id IS NOT NULL", verb, verb)
429429
if hit: return [('method', verb + (f" ({len(hit)} declarations)" if len(hit) > 1 else ''), [x['id'] for x in hit])]
430+
# …and a private FIELD (`Vault.#secret`, `#secret`) the same way: rewritten, it asked for `Vault..secret`
431+
rows = self.field_rows(verb) if kind in (None, 'field') and re.search(r'(^|\.)#[A-Za-z_$]', verb) else []
432+
if rows: return [('field', f"{rows[0]['kind']} {rows[0]['display']}" + (f" (+{len(rows)-1} declarations of that name)" if len(rows) > 1 else ''), rows)]
430433
base = re.sub(r'\(.*\)$', '', s).replace('#', '.').strip('.')
431434
if not (self.field_rows(base) or self.types(base, soft=True) or self.methods(base, soft=True)): base = base.replace('$', '.') # Outer$Inner — unless $ is part of the name ($Gson$Types)
432435
m0 = re.match(r'^(.+)\.<new>$', base)

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ A = {
123123
# each made the function or class it names look declared as a second kind, so `impact C` refused as ambiguous
124124
decls=[dict(file='all-javascript-variables.csv', only=lambda r: not r.get('ownerMethodLinkHash') and r.get('bindingRegime') not in ('IMPORT_BINDING', 'FUNCTION_DECLARATION_HOISTED', 'CLASS_TDZ') and not r.get('importLinkHash'),
125125
kind=lambda r: 'const' if r.get('bindingRegime', '').startswith('CONST') else 'variable', name='name', owner=None, fileVia=('modules', 'ownerModuleLinkHash'), line='startLine', end='endLine'),
126-
dict(file='all-javascript-fields.csv', kind=lambda r: 'field', name='name', owner=None, fileVia=('modules', 'ownerModuleLinkHash'), line='startLine', end='startLine')],
126+
# a class field names its class by type id, not qualified name: without it the field had no owner, so
127+
# `impact Vault.x` matched it only by qualified_name and every reader in the class was a name match at best
128+
dict(file='all-javascript-fields.csv', id='jsFieldUniqueHash', kind=lambda r: 'field', name='name', owner=None, ownerType='ownerTypeLinkHash',
129+
fileVia=('modules', 'ownerModuleLinkHash'), line='startLine', end='startLine')],
127130
# `const foo = () => …`: the variable's initializerExpressionLinkHash is the FUNCTION_EXPRESSION row whose
128131
# introducesDeclarationLinkHash is the method — two hops, where TypeScript has boundFunctionLinkHash in one
129132
boundNames=dict(file='all-javascript-variables.csv', name='name', only=lambda r: r.get('initializerKind') == 'FUNCTION',
@@ -372,7 +375,7 @@ for d in A['decls']:
372375
if d.get('only') and not d['only'](r): continue
373376
name = r.get(d['name'], '');
374377
if not name: continue
375-
fp = file_of(r, d); od = owner_display(r.get(d['owner'], '')) if d.get('owner') else None
378+
fp = file_of(r, d); od = owner_display(r.get(d['owner'], '')) if d.get('owner') else (tdisplay(r.get(d['ownerType'], '')) or None) if d.get('ownerType') else None
376379
ln = int(r.get(d['line']) or 0); en = int(r.get(d['end']) or ln)
377380
if (fp, ln, name) in bound_at: continue # `const foo = () => …` is already the function foo
378381
sym.append((r.get(d['id']) if d.get('id') else None, name, (od + '.' if od else '') + name, d['kind'](r), (r.get('qualifiedName') or ((r.get(d['owner'], '') + '.') if d.get('owner') else (fp + '#')) + name), None, fp, ln, en, od, 1 if fp and TESTRE.search(fp) else 0, None, None))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{"lang": "javascript", "src": "src",
2+
"checks": [
3+
{"why": "a private class field is written with its # (Vault.#secret): read as a member separator, the lookup asked for 'Vault..secret' and found nothing, though the field is in the graph under that very name",
4+
"run": ["impact", "Vault.#secret"],
5+
"want": ["change: field Vault.#secret", "Vault.reveal"],
6+
"avoid": ["nothing named", "[in scope] Ledger.peek", "[resolved] Ledger.peek"]},
7+
{"why": "the bare #secret names the private field too, in every class that declares one",
8+
"run": ["impact", "#secret"],
9+
"want": ["field Vault.#secret", "Vault.reveal", "Ledger.peek"],
10+
"avoid": ["nothing named"]},
11+
{"why": "control: a private METHOD written with its # still resolves to the method, not to a field",
12+
"run": ["impact", "Vault.#hide"],
13+
"want": ["change: Vault.#hide", "Vault.describe"],
14+
"avoid": ["field"]},
15+
{"why": "control: a public field is answered under its owner, and a sibling that does not touch it is not a reader",
16+
"run": ["impact", "Vault.label"],
17+
"want": ["change: field Vault.label", "Vault.#hide"],
18+
"avoid": ["[in scope] Vault.reveal", "[resolved] Vault.reveal"]}
19+
]}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class Vault {
2+
#secret = 1;
3+
label = 'vault';
4+
reveal() { return this.#secret; }
5+
#hide() { return this.label; }
6+
describe() { return this.#hide(); }
7+
}
8+
9+
export class Ledger {
10+
#secret = 2;
11+
peek() { return this.#secret; }
12+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{"lang": "typescript", "src": "src",
2+
"checks": [
3+
{"why": "a private class field is written with its # (Vault.#secret): read as a member separator, the lookup asked for 'Vault..secret' and found nothing, though the field is in the graph under that very name",
4+
"run": ["impact", "Vault.#secret"],
5+
"want": ["change: field Vault.#secret", "Vault.reveal"],
6+
"avoid": ["nothing named", "[in scope] Ledger.peek", "[resolved] Ledger.peek"]},
7+
{"why": "the bare #secret names the private field too, in every class that declares one",
8+
"run": ["impact", "#secret"],
9+
"want": ["field Vault.#secret", "Vault.reveal", "Ledger.peek"],
10+
"avoid": ["nothing named"]},
11+
{"why": "control: a private METHOD written with its # still resolves to the method, not to a field",
12+
"run": ["impact", "Vault.#hide"],
13+
"want": ["change: Vault.#hide", "Vault.describe"],
14+
"avoid": ["field"]},
15+
{"why": "control: a public field is answered under its owner, and a sibling that does not touch it is not a reader",
16+
"run": ["impact", "Vault.label"],
17+
"want": ["change: field Vault.label", "Vault.#hide"],
18+
"avoid": ["[in scope] Vault.reveal", "[resolved] Vault.reveal"]}
19+
]}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class Vault {
2+
#secret: number = 1;
3+
label: string = 'vault';
4+
reveal(): number { return this.#secret; }
5+
#hide() { return this.label; }
6+
describe() { return this.#hide(); }
7+
}
8+
9+
export class Ledger {
10+
#secret: number = 2;
11+
peek(): number { return this.#secret; }
12+
}

0 commit comments

Comments
 (0)