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:
Colby Mchenry
2026-10-01 03:52:28 +00:00
committed by GitHub
parent 8ca4433502
commit 3343646446
3 changed files with 75 additions and 6 deletions
+1
View File
@@ -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');
});
});
+14 -6
View File
@@ -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()