mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(nestjs): the provider convention stays in the reference's feature (#2246)
In a TS/JS module the *Service/*Controller convention picks the file's own class or the convention file beside it (users.controller.ts → users.service.ts), never a same-named class elsewhere preferred for its file name; anything farther is the import resolver's or name matching's. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In NestJS projects, the provider naming convention (`…Service`, `…Controller`, …) now picks a class only from the reference's own file. Another file's provider is reached through its import, or by ordinary name matching that prefers the nearest declaration. Before, the convention's preference for `*.service.ts` files sent Nest's own specs' `TransientService` and `TestService` to same-named classes in unrelated sample apps.
|
||||
- In Astro projects, the component-name convention now applies only inside `.astro` files, where components are used. Before, a TypeScript type with a component's name was linked to that component: astrowind's `image?: Image` and `callToAction?: CallToAction` in its type declarations, and starlight's Playwright `page: Page` in its tests.
|
||||
- In JavaScript and TypeScript, the Express conventions for middleware (`validate…`, `auth`, `logger`, …), controllers and services now pick only the calling file's own declaration. Another file's is reached only through an `import` or `require`. Before, SvelteKit's remote functions linked `validate(arg)` to its config loader's `validate` in another package, and Express's examples linked `logger('dev')` to one example's `logger`.
|
||||
- An Objective-C class that conforms to a protocol of its own name now links to that protocol instead of to itself. Swift's naming conventions for views, models and controllers also no longer apply to Objective-C code. Before, every one of SDWebImage's `@interface SDDiskCache : NSObject <SDDiskCache>`-style classes implemented itself.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* The NestJS provider convention (`*Service`, `*Controller`, …) picks a
|
||||
* class by name only from the reference's own file in a TS/JS module —
|
||||
* another file's comes through an import, the import resolver's. Its
|
||||
* file-name preference (`*.service.ts`) beat name matching's proximity:
|
||||
* Nest's own specs' `TransientService` / `TestService` went to same-named
|
||||
* classes in unrelated integration apps.
|
||||
*/
|
||||
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-nest-provider-'));
|
||||
const files: Record<string, string> = {
|
||||
'package.json': JSON.stringify({ name: 'nest', private: true, dependencies: { '@nestjs/common': '^10.0.0' } }),
|
||||
'integration/injector/src/app.module.ts': `import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class AppModule {}
|
||||
`,
|
||||
'integration/injector/src/scoped/transient.service.ts': `export class TransientService {}
|
||||
`,
|
||||
'packages/core/test/scope/helpers.ts': `export class TransientService {}
|
||||
`,
|
||||
'packages/core/test/scope/transient-scope.spec.ts': `class Holder {
|
||||
constructor(public transient: TransientService) {}
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 });
|
||||
});
|
||||
|
||||
describe('NestJS provider names', () => {
|
||||
it('never prefer another file’s class for its file name over a nearer one', () => {
|
||||
const ids = cg.getNodesInFile('packages/core/test/scope/transient-scope.spec.ts').map((n) => n.id);
|
||||
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.filePath);
|
||||
expect(targets).not.toContain('integration/injector/src/scoped/transient.service.ts');
|
||||
});
|
||||
});
|
||||
@@ -89,6 +89,19 @@ export const nestjsResolver: FrameworkResolver = {
|
||||
.getNodesByName(ref.referenceName)
|
||||
.filter((n) => n.kind === 'class');
|
||||
if (candidates.length === 0) return null;
|
||||
// A TS module reaches another file's class only by importing it — the
|
||||
// import resolver's — so a guess by name stays in the feature: the
|
||||
// file itself, or the convention file beside it (`users.controller.ts`
|
||||
// → `users.service.ts`). Every Nest sample app declares its own
|
||||
// `CatsService`, and the file-name preference sent specs across apps.
|
||||
const own = candidates.find((n) => n.filePath === ref.filePath);
|
||||
if (own) return { original: ref, targetNodeId: own.id, confidence: 0.85, resolvedBy: 'framework' };
|
||||
if (/\.(?:m?[jt]sx?|cjs|cts|mts)$/.test(ref.filePath)) {
|
||||
const dir = ref.filePath.slice(0, ref.filePath.lastIndexOf('/') + 1);
|
||||
const beside = candidates.find((n) => n.filePath.includes(convention) &&
|
||||
n.filePath.startsWith(dir) && !n.filePath.slice(dir.length).includes('/'));
|
||||
return beside ? { original: ref, targetNodeId: beside.id, confidence: 0.85, resolvedBy: 'framework' } : null;
|
||||
}
|
||||
const preferred = candidates.find((n) => n.filePath.includes(convention));
|
||||
const target = preferred ?? candidates[0]!;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user