mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
fix(php): a bare class name is its namespace's class or the imported one (#2235)
isPhpClassVisible: an unqualified PHP class name means the current namespace's class or the one a `use` imports (none in the project for a library's); applied before ranking and as a cross-file veto. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9d457d528
commit
3df191b6b9
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In PHP, an unqualified class name now resolves to the current namespace's class or the one a `use` imports, as PHP does. If the imported class comes from a library, it resolves to nothing in the project. Before, koel's form requests linked `extends Request` to whichever `Request` class was indexed first, and BookStack's controllers linked `Request $request` (Laravel's, by import) to BookStack's own `Request` subclass.
|
||||
- A call in C code is no longer linked to a C++ class's method, since C has no methods and can't call one. Before, redis's POSIX `read(fd, buf, n)` calls and hiredis's `c->funcs->read(…)` function pointer pointed at a Qt adapter's `read` method.
|
||||
- In Go, a name written through an imported package from outside the module, like `context.Context`, `http.ResponseWriter` or testify's `require.Contains`, is no longer linked to a project symbol of that name. Before, fiber's `context.Context` parameters pointed at a method `Stream.Context`, its `require.Contains(…)` assertions at an extractor's `Contains`, and gin's `http.ResponseWriter` at its own `ResponseWriter` interface.
|
||||
- Bare calls in Gradle Kotlin scripts (`build.gradle.kts`), like `plugins { }`, `dependencies { }` or `api(…)`, are no longer linked to a method or property of one of the project's own classes. Build logic's extension functions on Gradle's types still resolve. Before, nowinandroid's build scripts pointed every `plugins { }` and `dependencies { }` block at its build logic's `Graph` class, and every `api(…)` at a lint registry's `api` property.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* An unqualified PHP class name is the current namespace's class or the one
|
||||
* a `use` imports — PHP never falls back to another namespace for classes.
|
||||
* koel's `extends Request` (in `App\Http\Requests\API`, under `use
|
||||
* App\Http\Requests\API\Request;`, or under `use Saloon\Http\Request;`) all
|
||||
* went to the first `Request` indexed, and BookStack's controllers'
|
||||
* `Request $request` (`use Illuminate\Http\Request;`) to its own subclass.
|
||||
*/
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-php-ns-'));
|
||||
const files: Record<string, string> = {
|
||||
'app/Http/Requests/API/Interaction/Request.php': `<?php
|
||||
|
||||
namespace App\\Http\\Requests\\API\\Interaction;
|
||||
|
||||
abstract class Request {}
|
||||
`,
|
||||
'app/Http/Requests/API/Request.php': `<?php
|
||||
|
||||
namespace App\\Http\\Requests\\API;
|
||||
|
||||
abstract class Request {}
|
||||
`,
|
||||
'app/Http/Requests/API/DeleteSongsRequest.php': `<?php
|
||||
|
||||
namespace App\\Http\\Requests\\API;
|
||||
|
||||
class DeleteSongsRequest extends Request {}
|
||||
`,
|
||||
'app/Http/Requests/API/Playlist/PlaylistSongUpdateRequest.php': `<?php
|
||||
|
||||
namespace App\\Http\\Requests\\API\\Playlist;
|
||||
|
||||
use App\\Http\\Requests\\API\\Request;
|
||||
|
||||
class PlaylistSongUpdateRequest extends Request {}
|
||||
`,
|
||||
'app/Http/Integrations/SearchRequest.php': `<?php
|
||||
|
||||
namespace App\\Http\\Integrations;
|
||||
|
||||
use Saloon\\Http\\Request;
|
||||
|
||||
class SearchRequest extends Request {}
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const supertypesOf = (file: string) => {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'extends').map((e) => cg.getNode(e.target)!.qualifiedName);
|
||||
};
|
||||
|
||||
describe('bare PHP class names', () => {
|
||||
it('are the namespace’s own class', () => {
|
||||
expect(supertypesOf('app/Http/Requests/API/DeleteSongsRequest.php')).toEqual(['App\\Http\\Requests\\API::Request']);
|
||||
});
|
||||
|
||||
it('or the one a use imports', () => {
|
||||
expect(supertypesOf('app/Http/Requests/API/Playlist/PlaylistSongUpdateRequest.php')).toEqual(['App\\Http\\Requests\\API::Request']);
|
||||
});
|
||||
|
||||
it('and nothing in the project when the import is a library’s', () => {
|
||||
expect(supertypesOf('app/Http/Integrations/SearchRequest.php')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1336,6 +1336,25 @@ function isGoExternalQualified(ref: UnresolvedRef, context: ResolutionContext):
|
||||
return external;
|
||||
}
|
||||
|
||||
const PHP_CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'interface', 'trait', 'enum']);
|
||||
/**
|
||||
* Whether a bare PHP class name at `ref` can mean `candidate`. An unqualified
|
||||
* class name is the current namespace's class or the one a `use` imports —
|
||||
* PHP never falls back to another namespace for classes. koel's `extends
|
||||
* Request` (under `use Saloon\Http\Request;`, `use App\Http\Requests\API\Request;`,
|
||||
* or in `App\Http\Requests\API` itself) all went to the first `Request` indexed.
|
||||
*/
|
||||
function isPhpClassVisible(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (ref.language !== 'php' || candidate.language !== 'php' || !PHP_CLASS_KINDS.has(candidate.kind)) return true;
|
||||
const name = ref.referenceName;
|
||||
if (!/^[A-Za-z_]\w*$/.test(name) || /^(?:self|static|parent)$/i.test(name)) return true;
|
||||
const fqn = candidate.qualifiedName.replace(/::/g, '\\');
|
||||
const scope = phpFileScope(ref.filePath, context);
|
||||
const imported = scope.uses.get(name);
|
||||
if (imported !== undefined) return imported.toLowerCase() === fqn.toLowerCase();
|
||||
return fqn.toLowerCase() === (scope.namespace ? `${scope.namespace}\\${name}` : name).toLowerCase();
|
||||
}
|
||||
|
||||
const SCALA_OBJECT_PACKAGES = new WeakMap<ResolutionContext, Map<string, string | null>>();
|
||||
|
||||
/** The full package a Scala file's `package object X` opens (`algebra`, `cats.syntax`), or null for none. */
|
||||
@@ -1464,6 +1483,8 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
|
||||
// private to the component: shadcn-svelte's 838 `<Item.Root>` (a namespace
|
||||
// import) went to a `type Item` one example component declares for itself.
|
||||
if (isSfcPrivate(candidate, context)) return false;
|
||||
// A bare PHP class name is its namespace's class, or the one a `use` names.
|
||||
if (!isPhpClassVisible(candidate, ref, context)) return false;
|
||||
// A Scala package object's member is in scope in its package and those under
|
||||
// it, or through an import: cats.laws' `Eq` is the `cats` package object's
|
||||
// alias, not the `algebra` one's (752 refs went there).
|
||||
@@ -4814,6 +4835,10 @@ export function matchByExactName(
|
||||
// A C# type name is a type its namespaces can see — ahead of the ranking,
|
||||
// so a visible namesake wins where the veto after it would drop the
|
||||
// ref: eShop's `WebhookType.OrderPaid` under `using Webhooks.API.Model;`.
|
||||
// A bare PHP class name, only its namespace's or the imported one — ahead of
|
||||
// the ranking, so koel's `extends Request` under `use App\Http\Requests\API\Request;`
|
||||
// is that class, not the first `Request` indexed.
|
||||
isPhpClassVisible(n, ref, context) &&
|
||||
// A Scala package object's member, only where it is in scope — ahead of
|
||||
// the ranking, so cats.laws' `Eq` can be the `cats` package object's.
|
||||
!(ref.language === 'scala' && n.language === 'scala' && n.filePath !== ref.filePath &&
|
||||
|
||||
Reference in New Issue
Block a user