From a2c47e2a77c824209fe952b91a4812adf11d59db Mon Sep 17 00:00:00 2001 From: swapnil Date: Wed, 30 Sep 2026 01:36:39 -0700 Subject: [PATCH] impact: a TypeScript object literal key survives a bound access of a same-named field on its line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What was wrong - `const meta: EventMeta = { eventId: options.eventId ?? … }` stopped listing its function under `impact EventMeta.eventId`. The per-line rule that drops a name match on a line where the engine bound a field access (to this field or to another field of that name) drops every ref of that name on the line. Refs carry no column, and TypeScript stored the literal key `eventId` as a plain UNKNOWN identifier, so the key went with the bound `options.eventId` read of PublishOptions.eventId. field_access has no row for an object literal key, so nothing else reported the write. The change - axiomcode-index: a TypeScript expression in the OBJECT_PROPERTY_KEY role is stored with entity kind OBJECT_PROPERTY_KEY instead of UNKNOWN. - dl/impact.dl: fref keeps a ref of that kind past fa_line. A bound access on another line, and the bound access itself, are still not this field's readers. - IMPACT_VERSION 56. tests/cases/typescript/object-key-beside-a-bound-access: red before, green after, with two controls (a line that only reads the other type's field stays out; the bound read stays the other field's resolved reader). tests/run.py --lang typescript: 204 of 204. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/axiomcode-impact | 2 +- .../skills/axiomcode/scripts/axiomcode-index | 5 +++++ .../skills/axiomcode/scripts/dl/impact.dl | 5 ++++- .../object-key-beside-a-bound-access/case.json | 14 ++++++++++++++ .../object-key-beside-a-bound-access/src/bus.ts | 8 ++++++++ .../src/recorder.ts | 7 +++++++ .../src/stamper.ts | 7 +++++++ .../object-key-beside-a-bound-access/src/types.ts | 8 ++++++++ 8 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/cases/typescript/object-key-beside-a-bound-access/case.json create mode 100644 tests/cases/typescript/object-key-beside-a-bound-access/src/bus.ts create mode 100644 tests/cases/typescript/object-key-beside-a-bound-access/src/recorder.ts create mode 100644 tests/cases/typescript/object-key-beside-a-bound-access/src/stamper.ts create mode 100644 tests/cases/typescript/object-key-beside-a-bound-access/src/types.ts diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 8d2b5ef4..4e300359 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -1101,7 +1101,7 @@ class Impact: W('cs_fixture_type', sorted(x for x in fixt if not x[0].startswith('collection:'))) # ── facts: the graph, exported once (reused while graph.sqlite is unchanged) ──────────────────────────────── - IMPACT_VERSION = '56' # 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key + IMPACT_VERSION = '57' # 57: a TypeScript object literal key is a ref of entity kind OBJECT_PROPERTY_KEY, kept past a bound access on its line; 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key # layer; 15: the registration facts (two 14s landed independently, which is exactly the collision this # guards); 16: regsite folded into ax_registration's reg_key_fact; 20: implements_pair (#1011); 17/18: the tagged-template test registrar # (it.each`…`) and its table span diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index index 2bbd460a..830ba9a5 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index @@ -94,6 +94,9 @@ A = { # a property's name (`this.pending`, `row.setX`) is in literalValue with an empty potentialQualifiedName — 36k of 60k references in the parser repo expr=dict(file='all-typescript-expressions.csv', kind='kind', name='potentialQualifiedName', nameFallback='literalValue', line='startLine', fileVia=('modules', 'tsModuleLinkHash'), refKinds={'IDENTIFIER_REFERENCE', 'PROPERTY_ACCESS'}, entityKind='referencedEntityKind', + # the key of an object literal (`{ eventId: … }`) is stored with the entity kind OBJECT_PROPERTY_KEY, not + # UNKNOWN: it is never a property ACCESS, so an access the engine bound on the same line is not this name + keyRole=dict(role='edgeRole', value='OBJECT_PROPERTY_KEY'), litKinds={'LITERAL'}, litType=('literalType', 'STRING'), litValue='literalValue'), comments=dict(file='all-typescript-comments.csv', text='commentText', kind='commentKind', line='startLine', filePath='filePath'), typeRefs=dict(file='all-typescript-type-references.csv', name='typeName', context='context', ownerKind='referenceOwnerKind', line='startLine', fileVia=('modules', 'tsModuleLinkHash')), @@ -573,6 +576,7 @@ c.executemany("INSERT INTO symbols VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", sym) e = A['expr']; refs = []; lits = [] namecol = e['name'] if isinstance(e['name'], str) else dict(x.split(':') for x in e['name']) mname = e.get('memberName'); child_ek = {} +kr = e.get('keyRole') if mname: # the member-name child's entity kind, for the access that stands for it (the parent's own is UNKNOWN until resolved) for r in rows(e['file']): if r.get(mname['role']) == mname['value'] and r.get(mname['parent']): child_ek[r[mname['parent']]] = r.get(e['entityKind'], '') @@ -591,6 +595,7 @@ for r in rows(e['file']): n = n.rsplit('.', 1)[-1] ek = r.get(e['entityKind'], '') if mname and ek in ('', 'UNKNOWN'): ek = child_ek.get(r.get(mname['id'], ''), ek) or ek + if kr and ek in ('', 'UNKNOWN') and r.get(kr['role']) == kr['value']: ek = kr['value'] refs.append((n, file_of(r, e), int(r.get(e['line']) or 0), k, ek)) elif k in e['litKinds'] and r.get(e['litType'][0]) == e['litType'][1]: v = (r.get(e['litValue']) or '') diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index 1c15fb72..c351aa14 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -462,7 +462,7 @@ direct(q, c, "uses", why, "by name", f, l) :- valueref(q, c, f, l), registered(q // a FIELD: references by name, judged by where they are and how they are written .decl fref(q:symbol, c:symbol, rk:symbol, f:symbol, l:number) fref(q, c, rk, f, l) :- target(q, "field", fl, _), field(fl, _, n, ff, fll), ref(c, n, rk, ek, f, l), !local_kind(ek), !type_or_call_kind(ek), (f != ff ; l != fll), - !fa_line(q, f, l). + (!fa_line(q, f, l) ; ek = "OBJECT_PROPERTY_KEY"). // an enum member is written like a type, so the parser labels the genuine reference TYPE: keep those, but only in a // file that can see the enum — its own directory, or a file that names the enum type somewhere .decl enum_member_target(q:symbol, fl:symbol) @@ -533,6 +533,9 @@ direct(q, c, "uses", "reads it", "resolved", f, l) :- target(q, "field", fl // more, whichever callable the line is attributed to. fa_known works per caller, and a lambda written on the same line // as the access (`m.GetOrAdd(T.Culture + k, x => ...)`) is a different callable, so its name match on T.Culture came // back as a second, [in scope] reader that reads nothing (#1445). +// An object literal's KEY on that line is not the access the engine bound, and no field_access row ever covers one: +// `const meta: EventMeta = { eventId: options.eventId }` binds `options.eventId` to PublishOptions.eventId, and the key +// `eventId` is EventMeta's. fref keeps a ref of entity kind OBJECT_PROPERTY_KEY (TypeScript) past fa_line. .decl fa_line(q:symbol, f:symbol, l:number) fa_line(q, f, l) :- target(q, "field", fl, _), fa_bound(_, fl, _, f, l). fa_line(q, f, l) :- target(q, "field", fl, _), field(fl, _, n, _, _), fa_bound(_, fl2, _, f, l), fl2 != fl, field(fl2, _, n, _, _). diff --git a/tests/cases/typescript/object-key-beside-a-bound-access/case.json b/tests/cases/typescript/object-key-beside-a-bound-access/case.json new file mode 100644 index 00000000..58ec1090 --- /dev/null +++ b/tests/cases/typescript/object-key-beside-a-bound-access/case.json @@ -0,0 +1,14 @@ +{"lang": "typescript", "src": "src", + "checks": [ + {"why": "an object literal key on the same line as a bound access of another type's same-named field is a separate reference: `{ eventId: options.eventId }` typed EventMeta writes EventMeta.eventId, and the engine binding `options.eventId` to PublishOptions.eventId must not hide it", + "run": ["impact", "EventMeta.eventId"], + "want": ["change: field EventMeta.eventId", "Bus.publish", "Recorder.wrap"], + "avoid": ["Stamper.stamp"]}, + {"why": "control: a line that only reads another type's same-named field, bound by the engine, is still not this field's reader", + "run": ["impact", "EventMeta.eventId"], + "avoid": ["src/stamper.ts"]}, + {"why": "control: the bound access is the other field's resolved read", + "run": ["impact", "PublishOptions.eventId"], + "want": ["[resolved] Bus.publish src/bus.ts:5 — reads it", "[resolved] Stamper.stamp src/stamper.ts:5 — reads it"], + "avoid": ["Recorder.wrap"]} + ]} diff --git a/tests/cases/typescript/object-key-beside-a-bound-access/src/bus.ts b/tests/cases/typescript/object-key-beside-a-bound-access/src/bus.ts new file mode 100644 index 00000000..09e9a5b3 --- /dev/null +++ b/tests/cases/typescript/object-key-beside-a-bound-access/src/bus.ts @@ -0,0 +1,8 @@ +import { EventMeta, PublishOptions } from './types'; + +export class Bus { + publish(name: string, options: PublishOptions = {}): EventMeta { + const meta: EventMeta = { eventId: options.eventId ?? name, name }; + return meta; + } +} diff --git a/tests/cases/typescript/object-key-beside-a-bound-access/src/recorder.ts b/tests/cases/typescript/object-key-beside-a-bound-access/src/recorder.ts new file mode 100644 index 00000000..8af5d765 --- /dev/null +++ b/tests/cases/typescript/object-key-beside-a-bound-access/src/recorder.ts @@ -0,0 +1,7 @@ +import { EventMeta } from './types'; + +export class Recorder { + wrap(meta: EventMeta): string { + return meta.eventId; + } +} diff --git a/tests/cases/typescript/object-key-beside-a-bound-access/src/stamper.ts b/tests/cases/typescript/object-key-beside-a-bound-access/src/stamper.ts new file mode 100644 index 00000000..6a0c0b0a --- /dev/null +++ b/tests/cases/typescript/object-key-beside-a-bound-access/src/stamper.ts @@ -0,0 +1,7 @@ +import { PublishOptions } from './types'; + +export class Stamper { + stamp(options: PublishOptions): string { + return options.eventId ?? 'none'; + } +} diff --git a/tests/cases/typescript/object-key-beside-a-bound-access/src/types.ts b/tests/cases/typescript/object-key-beside-a-bound-access/src/types.ts new file mode 100644 index 00000000..c0cbcec2 --- /dev/null +++ b/tests/cases/typescript/object-key-beside-a-bound-access/src/types.ts @@ -0,0 +1,8 @@ +export interface EventMeta { + readonly eventId: string; + readonly name: string; +} + +export interface PublishOptions { + readonly eventId?: string; +}