mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 03:08:45 +08:00
fix(express): name patterns pick only the calling file's own declaration (#2243)
The middleware/controller/service heuristics took the first same-named node anywhere; a JS/TS module reaches another file's only through an import or require (the import resolver's), so they now keep a same-file pick only. 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 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.
|
||||
- In Solidity, a call to an overloaded function now reaches the overload whose parameter count it fits, as in the other languages with overloading. Before, OpenZeppelin's `_checkRole(role, _msgSender())` inside the one-argument `_checkRole(bytes32 role)` was linked to itself.
|
||||
- In JavaScript and TypeScript, the React conventions for hooks (`use…`) and contexts (`…Context`, `…Provider`) now pick only from the reference's own file. Another file's hook or context is reached only through an import. An optional call like `onSuccess?.(…)` now gets the same scope rules as a plain call. Before, trpc's WebSocket adapter's `createContext?.(…)` option linked to an example app's `createContext`, zod's `schemas.ParseContext` linked to the v3 copy instead of v4's, and bulletproof-react's apps linked `useUser()` to one another's.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* The Express resolver's middleware / controller / service name patterns pick
|
||||
* only the calling file's own declaration — another file's comes through an
|
||||
* `import` / `require`, the import resolver's. SvelteKit's remote functions'
|
||||
* `validate(arg)` went to the config loader's `validate` in another package,
|
||||
* and express' examples' `logger('dev')` to one example's `logger`.
|
||||
*/
|
||||
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-express-heur-'));
|
||||
const files: Record<string, string> = {
|
||||
'package.json': JSON.stringify({ name: 'app', private: true, dependencies: { express: '^4.0.0' } }),
|
||||
'packages/kit/src/core/config/options.js': `export function validate(config) {
|
||||
return config;
|
||||
}
|
||||
`,
|
||||
'packages/kit/src/runtime/remote/query.js': `export function query(validate, arg) {
|
||||
return () => validate(arg);
|
||||
}
|
||||
`,
|
||||
'server/app.js': `const express = require('express');
|
||||
const app = express();
|
||||
|
||||
function validateBody(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
app.post('/items', validateBody, (req, res) => res.send('ok'));
|
||||
`,
|
||||
};
|
||||
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 targetFilesFrom = (file: string) => {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind !== 'contains').map((e) => cg.getNode(e.target)!.filePath);
|
||||
};
|
||||
|
||||
describe('Express name patterns', () => {
|
||||
it('never reach another file’s declaration by name', () => {
|
||||
expect(targetFilesFrom('packages/kit/src/runtime/remote/query.js')).not.toContain('packages/kit/src/core/config/options.js');
|
||||
});
|
||||
});
|
||||
@@ -128,9 +128,16 @@ export const expressResolver: FrameworkResolver = {
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// A JS/TS module reaches another file's middleware, controller or service
|
||||
// only through an `import` / `require` — the import resolver's — never by
|
||||
// name: SvelteKit's remote functions' `validate(arg)` went to the config
|
||||
// loader's `validate` in another package. The patterns below stay for the
|
||||
// file's own declarations.
|
||||
const sameFile = (id: string | null): string | null =>
|
||||
id !== null && context.getNodeById?.(id)?.filePath === ref.filePath ? id : null;
|
||||
// Pattern 1: Middleware references
|
||||
if (isMiddlewareName(ref.referenceName)) {
|
||||
const result = resolveMiddleware(ref.referenceName, context);
|
||||
const result = sameFile(resolveMiddleware(ref.referenceName, context, ref.filePath));
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -145,7 +152,7 @@ export const expressResolver: FrameworkResolver = {
|
||||
const controllerMatch = ref.referenceName.match(/^(\w+)Controller\.(\w+)$/);
|
||||
if (controllerMatch) {
|
||||
const [, controller, method] = controllerMatch;
|
||||
const result = resolveControllerMethod(controller!, method!, context);
|
||||
const result = sameFile(resolveControllerMethod(controller!, method!, context));
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -160,7 +167,7 @@ export const expressResolver: FrameworkResolver = {
|
||||
const serviceMatch = ref.referenceName.match(/^(\w+)(Service|Helper|Utils?)\.(\w+)$/);
|
||||
if (serviceMatch) {
|
||||
const [, name, suffix, method] = serviceMatch;
|
||||
const result = resolveServiceMethod(name! + suffix!, method!, context);
|
||||
const result = sameFile(resolveServiceMethod(name! + suffix!, method!, context));
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -475,10 +482,11 @@ function isMiddlewareName(name: string): boolean {
|
||||
*/
|
||||
function resolveMiddleware(
|
||||
name: string,
|
||||
context: ResolutionContext
|
||||
context: ResolutionContext,
|
||||
fromFile?: string,
|
||||
): string | null {
|
||||
// Try exact name first
|
||||
const candidates = context.getNodesByName(name);
|
||||
// Try exact name first — the calling file's own first.
|
||||
const candidates = context.getNodesByName(name).sort((a, b) => Number(b.filePath === fromFile) - Number(a.filePath === fromFile));
|
||||
const match = candidates.find((n) =>
|
||||
n.name.toLowerCase() === name.toLowerCase() ||
|
||||
n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
|
||||
|
||||
Reference in New Issue
Block a user