test(code-knowledge): make the assignment case pin the binding fix (#943)

* test(code-knowledge): make the assignment case pin the binding fix

`ast-swift-module-scope.test.ts` asserted 2 edges for `alias = work()`. The name
on the right sits in a call callee, a position both walkers skip -- so the case
produced the same count before and after the fix it guards, and passed on the
base commit as well.

The fixture becomes `alias = work` followed by `work()`, and the expectation 1
edge. Measured on the two walkers, one variable:

  base a504f8af  alias = work()  2 edges   the old assertion passed here
  base a504f8af  alias = work    0 edges   the new assertion fails here
  main f73493d4  alias = work    1 edge    the new assertion passes here

Whole file: 2 failed / 36 passed on the base walker (the second failure is the
initializer case #928 fixed), 38 passed / 0 failed on main.

No source change: the walker on main already reads the position correctly.

* test(code-knowledge): pin both sides of the assignment, not just the callee

`ast-swift-module-scope.test.ts` asserted 2 edges for `alias = work()`. The name
on the right sits in a call callee, a position both walkers skip -- so the case
produced the same count before and after the fix it guards, and passed on the
base commit as well.

The fixture became `alias = work` followed by `work()`, and the expectation 1
edge. The review on this PR then pointed out that this exercises the *value*
side of the assignment while the case name describes the *target* side, and that
a regression reading assignment targets as declarations would still pass it.
That is right, so this revision covers both positions instead of renaming past
one of them:

  `does not let an assignment value mention stand in for a binding`
      var alias = 0; alias = work; work()   -- renamed to match its fixture

  `does not let an assignment target stand in for a binding`
      alias = work; alias()                 -- new, with func alias() sibling

Measured on the two walkers, one variable:

  base a504f8af  alias = work()                2 edges  the old assertion passed
  base a504f8af  alias = work + work()         0 edges  value case fails here
  base a504f8af  alias = work + alias()        0 edges  target case fails here
  main f73493d4  alias = work + work()         1 edge   value case passes here
  main f73493d4  alias = work + alias()        1 edge   target case passes here

  both sides cross-file, alias = work; alias(); work()
    base a504f8af  0 edges        main f73493d4  2 edges

Whole file: 3 failed / 36 passed (39) on the base walker -- the initializer case
#928 fixed plus these two -- and 39 passed / 0 failed (39) on main.

The target case needs its own fixture rather than a second assertion on the value
one: the value fixture's target is a name a real `var` already bound, so both
walkers suppress the call there and the assertion cannot distinguish them.

No source change: the walker on main already reads the position correctly.
This commit is contained in:
Smilewithoutfalling
2026-10-01 20:25:14 +08:00
committed by GitHub
parent b2d3598b38
commit bec06b3d2b
+27 -7
View File
@@ -608,18 +608,38 @@ describe('Swift module-scope resolution yields to enclosing bindings', () => {
expect(references[0]?.to).toBe('Sources/App/Worker.swift');
});
it('does not let an assignment target stand in for a binding', async () => {
it('does not let an assignment value mention stand in for a binding', async () => {
const { result } = await extractFiles([
['Sources/App/Worker.swift', 'func work() -> Int { return 1 }\n'],
['Sources/App/Run.swift', 'func run() {\n var alias = 0\n alias = work()\n work()\n}\n'],
['Sources/App/Run.swift', 'func run() {\n var alias = 0\n alias = work\n work()\n}\n'],
]);
// The same position in an assignment rather than a declaration, and with no
// `let` in sight. A rule that had to be told about one statement form after
// another would need a third fix here; reading the position needs none.
// `alias = work` assigns to `alias`; `work` on the right is a *mention*.
// Reading that mention as if the assignment declared it shadowed the
// module-level `work`, and the call on the next line then resolved to
// nothing. A rule that had to be told about one statement form after another
// would need a third fix here; reading the position needs none.
const references = result.edges.filter((e) => e.relation === 'REFERENCES');
expect(references).toHaveLength(2);
for (const reference of references) expect(reference.to).toBe('Sources/App/Worker.swift');
expect(references).toHaveLength(1);
expect(references[0]?.to).toBe('Sources/App/Worker.swift');
});
it('does not let an assignment target stand in for a binding', async () => {
const { result } = await extractFiles([
['Sources/App/Alias.swift', 'func alias() -> Int { return 1 }\n'],
['Sources/App/Run.swift', 'func run() {\n alias = work\n alias()\n}\n'],
]);
// The same statement read from the other side. `alias` is the assignment
// *target* and `work` on the right is a mention, so neither side is a
// binding and `alias()` answers to the sibling file. The value-position case
// above cannot see this one: its target is a name a real `var` already
// bound, so a rule that read assignment targets as declarations too would
// leave that test green.
const references = result.edges.filter((e) => e.relation === 'REFERENCES');
expect(references).toHaveLength(1);
expect(references[0]?.from).toBe('Sources/App/Run.swift');
expect(references[0]?.to).toBe('Sources/App/Alias.swift');
});
it('resolves a call that passes its own name', async () => {