mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
* fix(php): import decomposition, enum cases, anonymous class scope — F53,F54,F55 (#1931) * chore: fix unused imports, format, rebuild gitnexus-shared for macro type * chore(bench): update PHP scope-capture baseline to CI-computed hash * fix(php): reviewer fixes — grouped prefix, dead code removal, test precision * feat: add F55 anonymous class pipeline test * chore: fix format and benchmark baseline * chore: regen PHP golden after F53/F54/F55 query changes * chore: remove pipeline test, add grouped-prefix test, update fingerprint * chore: remove unused beforeAll and path imports --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
938111ad45
commit
7cbc544299
8 changed files with 172 additions and 34 deletions
|
|
@ -34,9 +34,10 @@
|
|||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f."
|
||||
},
|
||||
"php": {
|
||||
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
|
||||
"fingerprint": "bc2c27c5ba26d5aea61142a2a99fb772222f5b969205260eb7a71b4c0bd73cdb",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04)."
|
||||
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04).",
|
||||
"_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class — fixture count 138→140, fingerprint drift expected."
|
||||
},
|
||||
"ruby": {
|
||||
"fingerprint": "b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753",
|
||||
|
|
|
|||
|
|
@ -56,10 +56,22 @@ export function splitNamespaceUseDeclaration(stmtNode: SyntaxNode): CaptureMatch
|
|||
return decomposeGrouped(stmtNode, groupNode, qualifier);
|
||||
}
|
||||
|
||||
// Single use clause (possibly aliased).
|
||||
const spec = parseSingleUseClause(stmtNode, qualifier);
|
||||
if (spec === null) return [];
|
||||
return [buildImportMatch(stmtNode, spec)];
|
||||
// Non-grouped: iterate namespace_use_clause children (one per comma-separated
|
||||
// import in `use A, B, C;`). The first import is also a namespace_use_clause
|
||||
// in the grammar version used by the monorepo; namespace_name child handling
|
||||
// was removed after confirming the clause wrapper covers all positions.
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (let i = 0; i < stmtNode.namedChildCount; i++) {
|
||||
const child = stmtNode.namedChild(i);
|
||||
if (child === null || child.type !== 'namespace_use_clause') continue;
|
||||
const spec = parseUseClause(child, qualifier);
|
||||
if (spec !== null) {
|
||||
out.push(buildImportMatch(stmtNode, spec));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Qualifier detection ────────────────────────────────────────────────────
|
||||
|
|
@ -81,29 +93,6 @@ function detectQualifier(node: SyntaxNode): PhpImportKind {
|
|||
return 'namespace';
|
||||
}
|
||||
|
||||
// ── Single clause parsing ──────────────────────────────────────────────────
|
||||
|
||||
function parseSingleUseClause(node: SyntaxNode, qualifier: PhpImportKind): PhpImportSpec | null {
|
||||
// A plain `namespace_use_declaration` has one or more
|
||||
// `namespace_use_clause` named children (each clause is one import,
|
||||
// comma-separated for multiple). For the single case there is one.
|
||||
const clause = findNamedChild(node, 'namespace_use_clause');
|
||||
if (clause !== null) return parseUseClause(clause, qualifier);
|
||||
|
||||
// Older grammar versions may put the qualified_name directly under
|
||||
// the declaration node. Check for a qualified_name or name child.
|
||||
const qualName = findNamedChild(node, 'qualified_name') ?? findNamedChild(node, 'name');
|
||||
if (qualName === null) return null;
|
||||
const source = qualName.text.trim();
|
||||
if (source === '') return null;
|
||||
return {
|
||||
kind: qualifier,
|
||||
source,
|
||||
name: lastSegment(source),
|
||||
atNode: node,
|
||||
};
|
||||
}
|
||||
|
||||
function parseUseClause(clause: SyntaxNode, qualifier: PhpImportKind): PhpImportSpec | null {
|
||||
// namespace_use_clause:
|
||||
// qualified_name (or name)
|
||||
|
|
@ -162,8 +151,10 @@ function decomposeGrouped(
|
|||
groupNode: SyntaxNode,
|
||||
outerQualifier: PhpImportKind,
|
||||
): CaptureMatch[] {
|
||||
// The prefix is the qualified_name that precedes the `{...}` group.
|
||||
const prefixNode = findNamedChild(stmtNode, 'qualified_name') ?? findNamedChild(stmtNode, 'name');
|
||||
// The prefix is the namespace_name that precedes the `{...}` group.
|
||||
// tree-sitter-php emits the leading path as a namespace_name child,
|
||||
// not qualified_name.
|
||||
const prefixNode = findNamedChild(stmtNode, 'namespace_name') ?? findNamedChild(stmtNode, 'name');
|
||||
const prefix = prefixNode?.text.trim() ?? '';
|
||||
|
||||
const out: CaptureMatch[] = [];
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const PHP_SCOPE_QUERY = `
|
|||
(interface_declaration) @scope.class
|
||||
(trait_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
(anonymous_class) @scope.class
|
||||
|
||||
(method_declaration) @scope.function
|
||||
(function_definition) @scope.function
|
||||
|
|
@ -74,6 +75,10 @@ const PHP_SCOPE_QUERY = `
|
|||
(enum_declaration
|
||||
name: (name) @declaration.name) @declaration.enum
|
||||
|
||||
;; Enum case
|
||||
(enum_case
|
||||
name: (name) @declaration.name) @declaration.const
|
||||
|
||||
;; ── Declarations — methods / functions / constructors ─────────────────────
|
||||
|
||||
(method_declaration
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/php-anonymous-class/anon.php
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/php-anonymous-class/anon.php
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
$service = new class {
|
||||
public function execute(): void {
|
||||
echo "running";
|
||||
}
|
||||
};
|
||||
$service->execute();
|
||||
14
gitnexus/test/fixtures/lang-resolution/php-coverage/enum-and-anon.php
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/php-coverage/enum-and-anon.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// F54 — enum with cases
|
||||
enum UserRole: string {
|
||||
case Admin = 'admin';
|
||||
case Editor = 'editor';
|
||||
case Viewer = 'viewer';
|
||||
}
|
||||
|
||||
// F55 — anonymous class with method
|
||||
$service = new class {
|
||||
public function execute(): void {
|
||||
echo "running";
|
||||
}
|
||||
};
|
||||
4
gitnexus/test/fixtures/lang-resolution/php-coverage/multi-import.php
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/php-coverage/multi-import.php
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
// F53 — comma-separated use statements
|
||||
use A, B, C;
|
||||
use Foo\Bar, Baz\Qux as Quux, X\Y;
|
||||
|
|
@ -43,6 +43,10 @@
|
|||
"captureGroups": 12,
|
||||
"digest": "1c47e7020939a6cd6bb18f3732b51a16f4c75ded0fe9883a87d6706a2c624d1f"
|
||||
},
|
||||
"php-anonymous-class/anon.php": {
|
||||
"captureGroups": 5,
|
||||
"digest": "0628d6520b02da49ace196f582704c3ffe88aadb3ea20cff642699aa122d5f7f"
|
||||
},
|
||||
"php-app/app/Contracts/Loggable.php": {
|
||||
"captureGroups": 7,
|
||||
"digest": "46cf7e385db7656514b796908134a50f4a536f3adda64196e17d8c54f5cd5e3a"
|
||||
|
|
@ -52,8 +56,8 @@
|
|||
"digest": "40bbdb3fc0ec9b5a9bb8fbdf4a053099ac926951ecd746f2436569add3f0e3d5"
|
||||
},
|
||||
"php-app/app/Enums/UserRole.php": {
|
||||
"captureGroups": 7,
|
||||
"digest": "aadf56b87d67191b13f4e3f9fae7e8097f4c8a81d13135645ceb8c5be1063a9b"
|
||||
"captureGroups": 10,
|
||||
"digest": "203eb6943ff7527cc52ad93df040a4d15dbcf16beb9e78914401069c54f20bb8"
|
||||
},
|
||||
"php-app/app/Models/BaseModel.php": {
|
||||
"captureGroups": 18,
|
||||
|
|
@ -143,6 +147,14 @@
|
|||
"captureGroups": 15,
|
||||
"digest": "26181127fe9e9bf04431a7cc801624bde9dd284049627bb7ca0a4c9234141eec"
|
||||
},
|
||||
"php-coverage/enum-and-anon.php": {
|
||||
"captureGroups": 9,
|
||||
"digest": "83318a2833a16be704f15c2a32c0bbc9cfe0aacf693d1b82413a2eabf5ffed24"
|
||||
},
|
||||
"php-coverage/multi-import.php": {
|
||||
"captureGroups": 7,
|
||||
"digest": "4d72fbfba40f2e083aa55d1c48bb500aeb7dd615de18b48b00eaca5b26a2001e"
|
||||
},
|
||||
"php-deep-field-chain/Models.php": {
|
||||
"captureGroups": 26,
|
||||
"digest": "f20d780f2bc95ed438047c8e9bb694f5a020454161d8a20f8d5e02be68e4a602"
|
||||
|
|
@ -265,7 +277,7 @@
|
|||
},
|
||||
"php-grouped-imports/app/Services/Main.php": {
|
||||
"captureGroups": 15,
|
||||
"digest": "65ea5a0c2fd2b523ab1bf2e3f632bd3debeb7613c88496362016f9e590b64972"
|
||||
"digest": "b315d87f85f0899ed3883ec529b86a57f2d1a88d41b2b615e707307e2889c049"
|
||||
},
|
||||
"php-local-shadow/app/Services/Main.php": {
|
||||
"captureGroups": 9,
|
||||
|
|
|
|||
104
gitnexus/test/integration/resolvers/php-coverage.test.ts
Normal file
104
gitnexus/test/integration/resolvers/php-coverage.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Regression tests for PHP scope-resolution coverage gaps (issue #1931).
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { emitPhpScopeCaptures } from '../../../src/core/ingestion/languages/php/index.js';
|
||||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F53 — comma-separated use statements
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F53 — comma-separated use declarations', () => {
|
||||
it('use A, B, C produces 3 import names', () => {
|
||||
const src = `<?php\nuse A, B, C;\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const importNames = matches.filter((m) => m['@import.name']).map((m) => m['@import.name'].text);
|
||||
// A, B, C should each appear
|
||||
expect(importNames).toContain('A');
|
||||
expect(importNames).toContain('B');
|
||||
expect(importNames).toContain('C');
|
||||
expect(importNames.length).toBe(3);
|
||||
});
|
||||
|
||||
it('use Foo\\Bar, Baz\\Qux as Quux produces correct names and aliases', () => {
|
||||
const src = `<?php\nuse Foo\\Bar, Baz\\Qux as Quux;\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const importNames = matches.filter((m) => m['@import.name']).map((m) => m['@import.name'].text);
|
||||
expect(importNames).toContain('Bar');
|
||||
expect(importNames).toContain('Quux');
|
||||
expect(importNames.length).toBe(2);
|
||||
// Check alias
|
||||
const aliasImport = matches.find((m) => m['@import.alias']);
|
||||
expect(aliasImport).toBeDefined();
|
||||
expect(aliasImport!['@import.alias'].text).toBe('Quux');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F54 — enum cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F54 — enum case declarations', () => {
|
||||
it('enum case captures @declaration.const with case name', () => {
|
||||
const src = `<?php\nenum Role { case Admin; case Editor; }\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const cases = matches.filter((m) => m['@declaration.const']);
|
||||
expect(cases.length).toBe(2);
|
||||
const names = cases.map((m) => m['@declaration.name'].text).sort();
|
||||
expect(names).toEqual(['Admin', 'Editor']);
|
||||
});
|
||||
|
||||
it('enum_declaration still emits @scope.class and @declaration.enum', () => {
|
||||
const src = `<?php\nenum Role { case Admin; }\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const scopes = matches.filter((m) => m['@scope.class']);
|
||||
const enumScope = scopes.find((m) => m['@scope.class']?.text.includes('enum'));
|
||||
expect(enumScope).toBeDefined();
|
||||
const enumDecl = matches.filter((m) => m['@declaration.enum']);
|
||||
expect(enumDecl.length).toBe(1);
|
||||
expect(enumDecl[0]['@declaration.name'].text).toBe('Role');
|
||||
});
|
||||
|
||||
it('backed enum (with value) still captures case name', () => {
|
||||
const src = `<?php\nenum Role: string { case Admin = 'admin'; }\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const cases = matches.filter((m) => m['@declaration.const']);
|
||||
expect(cases.length).toBe(1);
|
||||
expect(cases[0]['@declaration.name'].text).toBe('Admin');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F55 — anonymous class scope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F55 — anonymous class scope', () => {
|
||||
it('anonymous class emits @scope.class', () => {
|
||||
const src = `<?php\n$s = new class { public function run() {} };\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const classScopes = matches.filter((m) => m['@scope.class']);
|
||||
expect(classScopes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('method inside anonymous class has @scope.function', () => {
|
||||
const src = `<?php\n$s = new class { public function run() {} };\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const fnScopes = matches.filter((m) => m['@scope.function']);
|
||||
// Method-scoping regression guard — pre-existing, not F55-specific.
|
||||
expect(fnScopes.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F53 — grouped use prefix
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F53 — grouped use prefix', () => {
|
||||
it('use App\\Models\\{User} preserves namespace prefix', () => {
|
||||
const src = `<?php\nuse App\\Models\\{User};\n`;
|
||||
const matches = emitPhpScopeCaptures(src, 'test.php') as CaptureMatch[];
|
||||
const importSource = matches.find((m) => m['@import.source'])?.['@import.source']?.text;
|
||||
expect(importSource).toBe('App\\Models\\User');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue