mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(scope-resolution): a named receiver's member never resolves lexically (#2699) `lookupCore` Step 1 walked the lexical scope chain for every lookup, including explicit-receiver property reads. So `options.baseUrl` could bind to an unrelated function-local `const baseUrl` in the same file, and `config.extractVisibility(node)` to the enclosing class's own method. This is the residual half of the defect JS/TS block scopes narrowed in #2695. Blocks moved nested-block locals off the chain of a reference outside the block, which removed 114 false edges; a local declared directly in the function body stayed on it, and no amount of extra scopes reaches that case. Fixed at the cause instead: `recv.name` names a member of whatever `recv` denotes, so a binding of the bare tail name in an enclosing scope is never the right answer. Steps 2 and 3 (receiver type / owner members) are the legitimate routes. `this` and `self` are EXEMPT, and that exemption was measured, not assumed. Skipping Step 1 for every explicit receiver removed 711 edges on a 762-file corpus — but 2 of those were genuine: `self.srcIx` and `self.streamedAt(...)` after `const self = this`, reaching their own class's members through the class-body scope. For a self-receiver the members and the lexical chain legitimately overlap; for a named receiver they never do. Exempting the self names keeps both true edges and still removes 709 false ones, adding none. The removals were classified by reading source at the site, not by pattern- matching ids — an "is the target a member of the source's owner?" heuristic labelled 43 of them plausible and every one I then read was false: language = config.language; -> the class's own `language` dirMap.get(...) / exactMap.get(...) -> a sibling object-literal `get` return config.extractVisibility(n); -> the class's own method (self-edge) writer.close(); -> GraphEmitSink.close Residual, deliberately kept: a `this.x` read can still bind lexically to a same-named local. That is the price of the two true self-alias edges above. `INCREMENTAL_SCHEMA_VERSION` 19 -> 20: a v19 index holds these false CALLS/ACCESSES on every unchanged file and would keep serving them through the reuse gate. Test confirmed discriminating: it fails with the guard reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * fix(typescript,javascript): a generator expression binding is a Function node (#2693) `const g = function* () {}` matched none of the closure-binding definition rules — they covered `arrow_function` and `function_expression` only — so the binding emitted a `Const` node. `buildGraphTargetIndex` admits callable nodes only, so `g()` resolved to nothing. Same defect shape as the `var` case #2693 already fixed: a different grammar node for the same construct, and the resulting graph node was not callable. Adds the four variable-binding shapes in both languages: `const`/`let` and `var`, each plain and exported. Purely additive — no existing pattern is reordered or rewritten, because the #2687 pre-scan dedup is order-dependent and collapsing the value/callable pair depends on which match wins. Deliberately NOT covered, and the query comment says so: a generator in an object-literal pair or a HOC wrapper still falls through anonymous. Those are rarer, and each additional pattern is another chance to disturb the dedup. `SCHEMA_BUMP` 26 -> 27: definition captures are parse-time, so a warm parse cache would replay the old ones verbatim — `--force` does not clear it. Two tests confirmed discriminating (they fail with the patterns reverted), plus a guard that the already-working generator DECLARATION form is unaffected, since it shares the emit path these were inserted beside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * fix(ingestion): keep caller attribution in lockstep with definition ids (#2699) The definition phase appends `localIdentity` to a nested callable's own name segment (`run.save@3:2`); `findEnclosingFunctionId` did not, so the two phases derived different ids for the same callable. The failure mode is silent — the caller id names a node that does not exist, so the edge is dropped rather than reported — which is why the parse-worker docblock calls this pair a lockstep guarantee and asks that both phases derive the prefix from one place. The condition is now byte-identical to the definition phase's (`nestedPrefix !== undefined`), so the two cannot diverge again. Scope of the claim, stated plainly: no reproducing case was found, and this changes nothing measurable on a 762-file TypeScript corpus. TS/JS resolve callers through `resolveCallerGraphId` in the graph bridge, not this path; `findEnclosingFunctionId` serves the `callExtractor` languages, and the corpus does not exercise a nested callable there. The review that raised it (P3) observed zero dangling edges, and "zero dangling" is also what silently dropped edges look like — so this closes a documented contract rather than a demonstrated bug, and carries no test of its own. Rides the `SCHEMA_BUMP` 26 -> 27 in the preceding commit: caller attribution runs in the worker, so a warm parse cache would replay the old ids. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * docs(test): correct the block-scope header that this PR made false (#2699) Review finding (MEDIUM). The file header still described `lookupCore` Step 1 as walking the lexical chain for EVERY lookup, and called the function-body-local case "unchanged and still mis-resolves ... pre-existing and tracked separately". Commit59b892cain this same PR falsified both, and the describe block added ~80 lines lower in this same file asserts the opposite — a reader scoping future work from the header would have concluded the case was still open. Rewritten to state what the code does: Step 1 is skipped for a NAMED explicit receiver, the function-body case is fixed here, and the surviving residual is that a `this`/`self` read can still bind lexically to a same-named local — with the reason those two names are exempt (they keep the genuine `const self = this; self.member` reads that Step 1 resolves correctly). Also corrects a PRE-EXISTING staleness inherited from #2695 in the same paragraph block: "the genuine bare read of that same local must still emit its edge" describes a test that no longer exists, because TypeScript emits no `@reference.read` for bare identifiers at all. Fixed here rather than left adjacent to a freshly corrected sentence. Comments only — `detect_changes` reports 0 changed symbols across 1 file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * refactor(ingestion): give the nested-callable id rule one definition (#2699) Review finding (LOW): the lockstep change in this PR shipped without a test. The plan called for a unit test asserting the two id-derivation phases agree. Two things changed that plan during execution, both recorded here. FIRST — there are THREE phases, not two. Re-verifying the plan's assumption (`grep -n localIdentity`) found a third call site: the worker-path node-id derivation in `processFileGroup` (parse-worker.ts:2316), whose own comment already acknowledged the coupling. `impact` on `localIdentity` corroborates: three direct dependents, all in the Workers module. So the invariant three phases must agree on is now ONE function, `nestedCallableQualifiedName`, and divergence requires deleting a call rather than editing a duplicated expression. SECOND — the planned `_forTest` alias seam does not work for this module. `parse-worker.ts` posts a `ready` message to `parentPort` at module scope, so value-importing it from a unit test throws before any test runs; the existing unit tests that reference it use `import type` only, which erases. The rules therefore move to a new pure module, `workers/callable-id.ts`. That is what makes them testable at all, rather than merely commented. Pure refactor — no id changes. Verified by the suites that assert exact node ids (`Function:svc.ts:run.save@7:2`, `Function:c.php:run.$save@3:2`): 74/74 green, and `detect_changes` reports only the three expected symbols and the two `processFileGroup` flows `impact` predicted. The test pins both halves: the rule's contract, and a structural assertion that no site has re-inlined `${prefix}.${localIdentity(...)}` — the unit assertions alone would still pass if a fourth phase spelled the rule out by hand, which is exactly how the divergence arose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * 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 * fix(rust): resolve `Self` in scope-resolution type bindings (#2699) CI regression, caught by `tests / ubuntu / coverage` on13d5e738and traced to the named-receiver Step-1 skip earlier in this PR (59b892ca), not to the three commits above it — verified by reverting those three and reproducing the failure unchanged. `test/integration/resolvers/rust.test.ts > resolves fresh.validate() inside impl User via Self {} inference` failed: 192/192 on main, 191/192 on this branch. The fixture calls `fresh.validate()` where `let fresh = Self { .. }` inside `impl User` — a genuine call to `User::validate`, and a TRUE edge that the skip deleted. Root cause is a twin-channel disagreement, not the skip: - `type-extractors/rust.ts:142` substitutes `Self` -> the enclosing impl type into the TYPE-ENV channel via `findEnclosingImplType`. - `languages/rust/interpret.ts` recorded `@type-binding.type` verbatim, so the SCOPE-RESOLUTION channel bound `fresh: Self` — a type that does not exist, leaving the receiver's type unknown and Step 2 unable to resolve. `main` passed only because Step 1 still walked the lexical chain for named receivers: the impl scope binds `validate` by name, so the call resolved BY ACCIDENT. Stopping that walk turned a latent gap into a lost edge. The fix closes the gap rather than restoring the accident — `Self` is now substituted at capture-emit time in `languages/rust/captures.ts`, where the impl node is reachable, reusing the `findEnclosingImpl` + `syntheticCapture` idiom already in that file. CORRECTION to this PR's central claim. "709 removed / 0 added / 0 true edges lost" was measured on a 762-file TYPESCRIPT corpus and stated without that qualifier. Rust lost one true edge. The measurement stands for TypeScript; it did not generalise, and the PR body is being updated to say so. Scope of the breakage, measured rather than assumed: 1 failure in 2927 tests across all 51 resolver files. Every other language — Go, Java, C#, Kotlin, Swift, Python, PHP, Ruby, Dart, C++ — passes, which is why this is a targeted fix and not a revert of the skip. Re-baselined `bench/scope-capture` for RUST ONLY (655aed01 -> 7f1240b3); the other 14 language fingerprints are byte-identical. The drift is the intended output change and the reason is recorded in the baseline entry, per that file's own "explain, never re-baseline to make CI green" rule. Verified: rust resolvers 192/192; all 51 resolver files 2926 passed / 1 skipped / 0 failed; the 8 targeted suites 96/96; all 8 CI bench gates PASS; `tsc --noEmit` clean; `detect_changes` reports one touched symbol (`emitRustScopeCaptures`) and no affected flows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR * test(golden): refresh the Rust capture golden and the C# PDG snapshot (#2699) The two committed artifacts CI flagged after5f55fe46. They drifted for OPPOSITE reasons, so each was inspected before regenerating rather than refreshed on sight. RUST GOLDEN — drifted because5f55fe46CORRECTS the output. A `Self` type binding now records the enclosing impl's type instead of the literal `Self`, in both the `let x = Self { .. }` and `fn new() -> Self` forms. Blast radius verified exact: 5 fixtures drifted, all 5 contain `Self`, and every `Self`-bearing rust fixture is among them (rust-self-struct-literal, rust-constructor-type-inference, rust-default-constructor, rust-method-enrichment, rust-scoped-multi-file). C# PDG SNAPSHOT — drifted because the named-receiver Step-1 skip (59b892ca) REMOVED A FALSE EDGE. CALLS 7 -> 6, and the edge that went is: Demo.Resolve.Parse@142:12#1 -> Demo.Resolve.Parse@142:12#1 a self-call, from `int Parse(string v) => int.Parse(v);`. `int.Parse(v)` is System.Int32.Parse; the lexical chain was binding it to the enclosing local function that happens to also be called `Parse`. Same defect class as `writer.close()` -> GraphEmitSink.close. The snapshot's own comment says it exists so "a future refactor that silently rewires the C-family graph trips this gate" — it tripped correctly, and the rewiring is an improvement. Both failures were PRE-EXISTING on this PR from59b892ca, not from the three commits above it — verified by reverting those and reproducing unchanged. They went unseen because this PR's CI was never watched after its first push. Verified after regeneration, WITHOUT update flags so they must genuinely pass: rust-captures-golden 9/9; pipeline-pdg 31/31. The snapshot diff is 3 lines, all inside the C# entry — no other language's snapshot moved. `detect_changes` reports 0 changed symbols (test artifacts only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184RmD24KFJidYqpM7v3XjR --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
/**
|
|
* #2699 — JS/TS `statement_block` scopes, and the false ACCESSES edges they
|
|
* remove.
|
|
*
|
|
* Enabling `(statement_block) @scope.block` for TS/JS dropped 114 ACCESSES
|
|
* edges across a 762-file corpus with `added: 0`. That looked like a
|
|
* regression, so it was measured rather than assumed: all 274 emitting
|
|
* reference sites behind those 114 edges were classified by re-reading the
|
|
* source at the site. Every one of the 114 had at least one site of the form
|
|
* `receiver.name`, and none was bare-identifier-only. (269 sites classified as
|
|
* member reads outright; the 5 remaining were classifier artifacts — the name
|
|
* also occurred earlier on the line, as in `a.b.declLine` for `b` — and are
|
|
* member reads too.) So every dropped edge was a PROPERTY read
|
|
* (`options.baseUrl`) mis-resolving to an unrelated function-local `const` of
|
|
* the same name in the same file.
|
|
*
|
|
* The cause was not block-specific: `lookupCore` Step 1 walked the lexical
|
|
* chain for EVERY lookup, including explicit-receiver property reads, so
|
|
* `options.baseUrl` could bind to a local `baseUrl`. Block scopes narrowed
|
|
* that — they moved a nested-block local off the chain of any reference
|
|
* outside its block — but a local declared directly in the FUNCTION BODY
|
|
* stayed on it, and no amount of extra scopes reaches that case.
|
|
*
|
|
* That residual half is fixed here too: Step 1 is now skipped when the site
|
|
* has a NAMED explicit receiver, since `recv.name` addresses a member of
|
|
* whatever `recv` denotes and never a lexical binding of the bare tail name.
|
|
* The second describe below pins it. What remains, deliberately, is that a
|
|
* `this`/`self` read can still bind lexically to a same-named local — that is
|
|
* the price of keeping the genuine self-alias reads Step 1 resolves correctly
|
|
* (`const self = this; self.member`), which is why those two names are exempt.
|
|
*
|
|
* So these tests pin the change in BOTH directions: a property read must not
|
|
* reach a same-named local, and a real member read must still resolve through
|
|
* the receiver's own type. Deleting the block-scope capture fails the first;
|
|
* over-suppressing — dropping block bindings rather than scoping them, or
|
|
* skipping Step 1 for `this` as well — fails the second.
|
|
*/
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
|
import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js';
|
|
|
|
vi.setConfig({ testTimeout: 90_000 });
|
|
|
|
/** `ACCESSES` edges in a one-file repo, as `source -> target` id pairs. */
|
|
const accessEdgesFor = async (filename: string, source: string): Promise<string[]> => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-block-scope-'));
|
|
try {
|
|
fs.writeFileSync(path.join(dir, filename), source, 'utf-8');
|
|
const result = await runPipelineFromRepo(dir, () => {}, {
|
|
workerPoolSize: 1,
|
|
workerUrlForTest: DIST_WORKER_URL,
|
|
// `pruneLocalSymbols` drops inert function-local value symbols — ~94% of
|
|
// them on a real corpus — so in a two-line fixture the `const` under test
|
|
// is deleted before any edge can name it, and both arms return []. That
|
|
// is why earlier synthetic attempts at this edge class all read as "no
|
|
// difference". Keeping them is what makes the fixture discriminate.
|
|
keepLocalValueSymbols: true,
|
|
});
|
|
return result.graph.relationships
|
|
.filter((rel) => rel.type === 'ACCESSES')
|
|
.map((rel) => `${rel.sourceId} -> ${rel.targetId}`)
|
|
.sort();
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
};
|
|
|
|
// The member read sits OUTSIDE the block on purpose. Inside it, the block is on
|
|
// the reference's own lexical chain and the property would bind to the local in
|
|
// either arm — so an inside-the-block fixture cannot discriminate.
|
|
const SHADOWED = [
|
|
'export function pickBaseUrl(options: { baseUrl?: string }, fallback: string): string {',
|
|
' if (fallback.length > 0) {',
|
|
' const baseUrl = fallback.trim();',
|
|
' return baseUrl;',
|
|
' }',
|
|
' return options.baseUrl ?? fallback;',
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
|
|
const describeIfWorkerBuilt = distWorkerExists() ? describe : describe.skip;
|
|
|
|
describeIfWorkerBuilt('block scopes keep a property read off a same-named block local', () => {
|
|
it('TypeScript: `options.baseUrl` does not ACCESS the block-local `const baseUrl`', async () => {
|
|
const edges = await accessEdgesFor('pick.ts', SHADOWED);
|
|
|
|
expect(edges.filter((e) => e.endsWith('baseUrl') && e.includes('pickBaseUrl'))).toEqual([]);
|
|
});
|
|
|
|
it('TypeScript: a real property read still resolves past a same-named block local', async () => {
|
|
// Companion invariant, not a discriminating regression test: this edge is
|
|
// identical in both arms. It exists because the test above only proves an
|
|
// edge went away, which a change that dropped Block-kind bindings entirely
|
|
// would also satisfy. Asserting the surviving edge SET — exactly one, and
|
|
// pointing at the class property rather than the block local — is what
|
|
// separates "correctly scoped" from "deleted".
|
|
const edges = await accessEdgesFor(
|
|
'box.ts',
|
|
[
|
|
'export class Box {',
|
|
" baseUrl = 'https://example.com';",
|
|
' pick(fallback: string): string {',
|
|
' if (fallback.length > 0) {',
|
|
' const baseUrl = fallback.trim();',
|
|
' return baseUrl;',
|
|
' }',
|
|
' return this.baseUrl;',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
// Matched on the target rather than the whole id: the method node carries
|
|
// an overload index (`Box.pick#1`) that is orthogonal to what this pins.
|
|
expect(edges).toHaveLength(1);
|
|
expect(edges[0]).toContain('-> Property:box.ts:Box.baseUrl');
|
|
});
|
|
});
|
|
|
|
describeIfWorkerBuilt('a property read never resolves to a lexical binding of its own name', () => {
|
|
// The residual half. Block scopes moved a NESTED-block local off the chain of
|
|
// a reference outside that block, which removed 114 false edges on a 762-file
|
|
// corpus. A local declared directly in the FUNCTION BODY stayed on the chain,
|
|
// so `options.baseUrl` still bound to it — same defect, one scope level up,
|
|
// and not fixable by adding more scopes.
|
|
//
|
|
// Fixed in `lookupCore` instead: Step 1's lexical walk is skipped when the
|
|
// site has an explicit receiver. `recv.name` names a member of whatever
|
|
// `recv` denotes; a binding of the bare tail name in an enclosing scope is
|
|
// never the right answer.
|
|
|
|
it('TypeScript: `options.baseUrl` does not ACCESS a function-body-level `const baseUrl`', async () => {
|
|
const edges = await accessEdgesFor(
|
|
'body.ts',
|
|
[
|
|
'export function pick(options: { baseUrl?: string }, fallback: string): string {',
|
|
' const baseUrl = fallback.trim();',
|
|
' if (baseUrl.length > 0) return baseUrl;',
|
|
' return options.baseUrl ?? fallback;',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
expect(edges.filter((e) => e.endsWith('baseUrl'))).toEqual([]);
|
|
});
|
|
|
|
it('TypeScript: a real member read still resolves through the receiver type', async () => {
|
|
// The guard against over-suppression: skipping Step 1 must not take Steps
|
|
// 2 and 3 with it. `this.baseUrl` has an explicit receiver too, and it
|
|
// must still reach the class property.
|
|
const edges = await accessEdgesFor(
|
|
'recv.ts',
|
|
[
|
|
'export class Box {',
|
|
" baseUrl = 'https://example.com';",
|
|
' read(): string {',
|
|
' return this.baseUrl;',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
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 });
|
|
}
|
|
});
|
|
});
|