mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(scope-resolution): give PHP's $this the same self-receiver exemption (#2699)
Review finding (LOW). The Step-1 skip added in this PR exempts `this`/`self`,
but the receiver name arrives as the reference node's RAW SOURCE TEXT —
`extractExplicitReceiver` returns `cap.text` verbatim — so PHP's `$this->x`
presents as the string "$this" and matched neither entry. PHP was the one
supported language whose self-receiver got no exemption at all.
Measured, and the measurement is why this is framed as consistency rather
than a bug fix:
- Corpus delta ZERO. 762-file TypeScript corpus, CALLS+ACCESSES set diff:
13179 -> 13179, added 0, removed 0. So no INCREMENTAL_SCHEMA_VERSION bump
(stays 20), per the plan's decision rule.
- No PHP shape found that DISCRIMINATES. Both the simple `$this->prop` /
`$this->helper()` shapes and a closure reading `$this->…` inside a method
that also declares a same-named local produce byte-identical edge sets
with `$this` present and absent — Step 2 resolves the receiver's type
first. The added test is therefore labelled a COMPANION INVARIANT, exactly
as the `this.baseUrl` case beside it is, and does not claim to prove the
fix.
It is still worth making: the exemption is protective, and the 709-removed /
0-true-lost measurement that justified the narrow guard was TypeScript-only,
so PHP's safety was never established by evidence. This closes that by
construction.
Two corrections to what the plan assumed, both found by checking:
- The plan (and my first draft of this comment) claimed the codebase had no
precedent for handling a sigil'd receiver name. FALSE: `THIS_RECEIVERS` in
`core/ingestion/type-env.ts:244` has always listed `$this`, and it is the
ingestion-side twin of this very list. The precedent does not merely
exist, it validates the approach chosen here — list the spelling as data,
do not strip sigils.
- That twin also lists `Me`. Deliberately NOT mirrored: no entry in
`SupportedLanguages` is Visual Basic, so it could only ever exempt a
variable that happens to be called `Me`.
The two lists are otherwise the same set with nothing enforcing it — a fifth
instance of the twin-list drift class this PR keeps meeting. A drift guard is
the right fix and is out of scope here; noted for follow-up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR
This commit is contained in:
parent
de2c0c6966
commit
13d5e738c1
2 changed files with 83 additions and 1 deletions
|
|
@ -321,7 +321,31 @@ function resolveReceiverOwner(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const IMPLICIT_RECEIVERS: readonly string[] = Object.freeze(['self', 'this']);
|
||||
/**
|
||||
* Names that denote the enclosing instance rather than an arbitrary object.
|
||||
*
|
||||
* Two consumers, and both want the same set: `resolveReceiverOwner` above
|
||||
* tries them when no explicit receiver is present, and the Step-1 skip in
|
||||
* `lookupCore` exempts them because for a SELF receiver the members and the
|
||||
* lexical chain legitimately overlap — a class body is itself a scope that
|
||||
* binds its members — whereas for a named receiver they never do.
|
||||
*
|
||||
* `$this` is matched because the receiver name arrives as the reference node's
|
||||
* RAW SOURCE TEXT (`extractExplicitReceiver` returns `cap.text` verbatim), so
|
||||
* PHP's `$this->x` presents as `"$this"`, sigil included. Listing the spelling
|
||||
* keeps this a data table rather than a language switch — this module resolves
|
||||
* language behaviour through `providers.*` and `params` only (see the header)
|
||||
* — and it follows the ingestion-side twin, `THIS_RECEIVERS` in
|
||||
* `gitnexus/src/core/ingestion/type-env.ts`, which has always listed the
|
||||
* sigil'd spelling rather than stripping it. Stripping would carry the same
|
||||
* false-positive surface anyway (a JS variable literally named `$this`).
|
||||
*
|
||||
* That twin also lists `Me`, deliberately NOT mirrored here: no entry in
|
||||
* `SupportedLanguages` uses it, so it can only ever exempt a variable that
|
||||
* happens to be called `Me`. The two lists are otherwise the same set, and
|
||||
* nothing enforces that — see the drift guard noted in #2714.
|
||||
*/
|
||||
const IMPLICIT_RECEIVERS: readonly string[] = Object.freeze(['self', 'this', '$this']);
|
||||
|
||||
function lookupReceiverType(
|
||||
startScope: ScopeId,
|
||||
|
|
|
|||
|
|
@ -170,4 +170,62 @@ describeIfWorkerBuilt('a property read never resolves to a lexical binding of it
|
|||
expect(edges).toHaveLength(1);
|
||||
expect(edges[0]).toContain('-> Property:recv.ts:Box.baseUrl');
|
||||
});
|
||||
|
||||
it('PHP: `$this` is exempt from the skip, like `this` and `self`', async () => {
|
||||
// COMPANION INVARIANT, not a discriminating regression test — and that was
|
||||
// measured, not assumed. The receiver name arrives as raw source text, so
|
||||
// PHP's `$this->x` presents as `"$this"` and matched neither exempt name
|
||||
// until #2714; but no PHP shape tried here depends on Step 1. This fixture
|
||||
// (a closure reading `$this->…` inside a method that also declares a
|
||||
// same-named local) produces byte-identical edge sets with `$this` present
|
||||
// and absent from `IMPLICIT_RECEIVERS`, because Step 2 resolves the
|
||||
// receiver's type first.
|
||||
//
|
||||
// It is kept for the same reason the `this.baseUrl` case above is: the
|
||||
// exemption is protective. Every other language's self-receiver keeps its
|
||||
// Step-1 route, and the 762-file corpus that measured "0 true edges lost"
|
||||
// was TypeScript-only, so PHP's safety was never established by evidence.
|
||||
// This pins that PHP member resolution through a self-receiver keeps
|
||||
// working if Step 2's coverage ever changes.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-php-self-'));
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'Box.php'),
|
||||
[
|
||||
'<?php',
|
||||
'class Box {',
|
||||
" private $baseUrl = 'https://example.com';",
|
||||
' public function helper() {',
|
||||
' return 1;',
|
||||
' }',
|
||||
' public function read() {',
|
||||
" $baseUrl = 'shadow';",
|
||||
' $fn = function () {',
|
||||
' return $this->baseUrl . $this->helper();',
|
||||
' };',
|
||||
' return $fn() . $baseUrl;',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const result = await runPipelineFromRepo(dir, () => {}, {
|
||||
workerPoolSize: 1,
|
||||
workerUrlForTest: DIST_WORKER_URL,
|
||||
keepLocalValueSymbols: true,
|
||||
});
|
||||
const calls = result.graph.relationships
|
||||
.filter((rel) => rel.type === 'CALLS')
|
||||
.map((rel) => rel.targetId)
|
||||
.sort();
|
||||
|
||||
// `$this->helper()` inside the closure reaches the class method, and the
|
||||
// same-named local `$baseUrl` never becomes a call target.
|
||||
expect(calls).toContain('Method:Box.php:Box.helper#0');
|
||||
expect(calls.filter((t) => t.includes('baseUrl'))).toEqual([]);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue