feat(steps): a reply that sets no status is a 200; inline Express handlers keep their replies
- effects.ts implicitResponseStatus: a body-sending reply with no status in its chain (res.json / send / render, reply.send, c.json, NextResponse.json, JSONResponse / jsonify / render_template, Rails render, Laravel response()->json) is a 200; a variable status, end, sendStatus and redirects stay as they were - branch-guards callSiteInTree: a status set by the statement just before the reply (`res.status(202); res.json(user)`) is that reply's — looked back within the block, only a statement that IS the status call counts - steps.ts: explicit chain/args → set-before → implicit 200 - express.ts: an inline handler's reply calls (`res.status(404).json(…)`, `res.json(user)`) are references at their own line and column instead of framework noise, so the route's own reply box exists - tests: servers fixture (inline route's 200 beside the service's 404; a 202 set before), ui-effects - CHANGELOG Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
co-authored by
Claude Fable 5
parent
77dc0ad2eb
commit
02430ccc32
@@ -49,6 +49,34 @@ const RESERVED_CALLS = new Set([
|
||||
'Date', 'Math', 'JSON', 'Promise', 'require', 'fail', 'redirect',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The replies an inline handler makes — `res.status(404).json({…})`,
|
||||
* `res.json(user)`, `reply.send(…)`, `ctx.body = …` aside — as references the
|
||||
* Steps view's effect table reads at their own line and column. The body's
|
||||
* plain calls above skip these names as framework noise on purpose (they are
|
||||
* not the business flow); for the endpoint's contract they are the point.
|
||||
*/
|
||||
const REPLY_CALL = /\b(res|response|reply|rep|ctx)\s*\.\s*(?:[A-Za-z_$][\w$]*\s*\([^()]*\)\s*\.\s*)*([A-Za-z_$][\w$]*)\s*\(/g;
|
||||
function replyRefs(safe: string, bodyStart: number, bodyEnd: number, fromNodeId: string, filePath: string, language: 'typescript' | 'javascript'): UnresolvedRef[] {
|
||||
const out: UnresolvedRef[] = [];
|
||||
const body = safe.slice(bodyStart, bodyEnd);
|
||||
REPLY_CALL.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = REPLY_CALL.exec(body)) !== null) {
|
||||
const at = bodyStart + m.index;
|
||||
out.push({
|
||||
fromNodeId,
|
||||
referenceName: `${m[1]}.${m[2]}`,
|
||||
referenceKind: 'calls',
|
||||
line: safe.slice(0, at).split('\n').length,
|
||||
column: at - (safe.lastIndexOf('\n', at - 1) + 1),
|
||||
filePath,
|
||||
language,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export const expressResolver: FrameworkResolver = {
|
||||
name: 'express',
|
||||
languages: ['javascript', 'typescript'],
|
||||
@@ -169,9 +197,13 @@ export const expressResolver: FrameworkResolver = {
|
||||
const afterArrow = args.slice(arrowAt + 2);
|
||||
const braceAt = afterArrow.indexOf('{');
|
||||
let body = afterArrow;
|
||||
let bodyStart = openParen + 1 + arrowAt + 2;
|
||||
if (braceAt >= 0 && afterArrow.slice(0, braceAt).trim() === '') {
|
||||
const end = matchDelim(afterArrow, braceAt, '{', '}');
|
||||
if (end > braceAt) body = afterArrow.slice(braceAt + 1, end);
|
||||
if (end > braceAt) {
|
||||
body = afterArrow.slice(braceAt + 1, end);
|
||||
bodyStart += braceAt + 1;
|
||||
}
|
||||
}
|
||||
const callRe = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
||||
const seen = new Set<string>();
|
||||
@@ -190,6 +222,7 @@ export const expressResolver: FrameworkResolver = {
|
||||
language: lang,
|
||||
});
|
||||
}
|
||||
references.push(...replyRefs(safe, bodyStart, bodyStart + body.length, routeNode.id, filePath, lang));
|
||||
} else {
|
||||
// Named handler: the LAST comma-separated arg (earlier ones are middleware).
|
||||
const parts = args.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
@@ -248,6 +281,7 @@ export const expressResolver: FrameworkResolver = {
|
||||
seen.add(name);
|
||||
references.push({ fromNodeId: routeNode.id, referenceName: name, referenceKind: 'calls', line, column: 0, filePath, language: lang });
|
||||
}
|
||||
references.push(...replyRefs(safe, openParen + 1, closeParen, routeNode.id, filePath, lang));
|
||||
} else {
|
||||
const parts = splitTopLevel(args).map((s) => s.trim()).filter(Boolean);
|
||||
const last = parts[parts.length - 1];
|
||||
|
||||
Reference in New Issue
Block a user