Files
codegraph/__tests__/express-inline-function-handler.test.ts
T
Colby Mchenry d45edca3fd fix(express): a function-expression handler is the route's body, like an arrow (#2261)
An Express handler written inline as `function (req, res) {…}` — the form
Express's own examples use — fell to the named-handler branch, so the route
linked to nothing and Steps drew a lone box for `POST /login`. The handler
is now the registration's LAST argument, inline as an arrow or a function
expression, bare or inside a wrapper call, past a trailing comma; an inline
middleware before a named handler no longer stands in for it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:11:26 +00:00

107 lines
3.0 KiB
TypeScript

/**
* An Express handler written inline as `function (req, res) {…}` — the form
* Express's own examples use — is the route's body just like an arrow is:
* its calls are the route's, so Steps draws what `POST /login` does instead
* of a lone box. The handler is the LAST argument; an inline middleware
* before a named handler is not it, and a trailing comma changes nothing.
*/
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-fnexpr-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'app', private: true, dependencies: { express: '^4.0.0' } }),
'app.js': `const express = require('express');
const app = express();
const router = express.Router();
function authenticate(name, pass, fn) {
fn(null, { name });
}
function restrict(req, res, next) {
next();
}
function loadUser(id) {
return { id };
}
function listPosts() {
return [];
}
function wrap(fn) {
return fn;
}
function showPost(req, res) {
res.send('post');
}
app.post('/login', function (req, res, next) {
authenticate(req.body.username, req.body.password, function (err, user) {
res.redirect('/');
});
});
app.get('/restricted', restrict, function named(req, res) {
res.send(loadUser(req.params.id));
});
router.get(
'/posts',
wrap(async function (req, res) {
res.json(listPosts());
}),
);
app.get('/post/:id', (req, res, next) => next(), showPost);
router.route('/users/:id').get(function (req, res) {
res.json(loadUser(req.params.id));
});
`,
};
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 fromRoute = (name: string, kind: string) => {
const route = cg.getNodesByKind('route').find((r) => r.name === name);
expect(route, name).toBeDefined();
return cg.getOutgoingEdgesFrom([route!.id]).filter((e) => e.kind === kind).map((e) => cg.getNode(e.target)!.name);
};
describe('Express function-expression handlers', () => {
it('lend the route their calls', () => {
expect(fromRoute('POST /login', 'calls')).toContain('authenticate');
expect(fromRoute('GET /restricted', 'calls')).toContain('loadUser');
expect(fromRoute('GET /users/:id', 'calls')).toContain('loadUser');
});
it('are found inside a wrapper call, past a trailing comma', () => {
expect(fromRoute('GET /posts', 'calls')).toContain('listPosts');
});
it('leave a named last argument the handler, even after an inline middleware', () => {
expect(fromRoute('GET /post/:id', 'references')).toContain('showPost');
expect(fromRoute('GET /post/:id', 'calls')).toEqual([]);
});
});