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; +}