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
@@ -501,9 +501,37 @@ export function callSiteInTree(root: SyntaxNode, source: string, line: number, c
|
||||
if (status === null && c.type !== 'comment') status = statusPropertyIn(c);
|
||||
}
|
||||
const text = parts.join(', ');
|
||||
if (status === null) status = statusSetBefore(call, callee);
|
||||
return { callee, args: text.length > MAX_ARGS_TEXT ? `${text.slice(0, MAX_ARGS_TEXT - 1)}…` : text, argList: parts, ...(status !== null ? { status } : {}) };
|
||||
}
|
||||
|
||||
const BODY_REPLY = /^(?:res|response|reply|rep|ctx|c|context)\.(?:json|jsonp|send|render|sendFile|download|end|text|html|body)$/;
|
||||
const STATEMENT_BLOCKS: ReadonlySet<string> = new Set(['statement_block', 'program', 'block', 'class_body', 'module']);
|
||||
/** Statements looked back through for a status the reply's own chain does not carry. */
|
||||
const STATUS_LOOKBACK = 6;
|
||||
|
||||
/**
|
||||
* `res.status(202); res.json(user)` — the status set by an earlier statement
|
||||
* in the same block, when the reply's own chain sets none. Only a statement
|
||||
* that IS the status call counts (`res.status(404)` inside an `if` before it
|
||||
* is another path, not this reply's); the first one found walking back wins.
|
||||
*/
|
||||
function statusSetBefore(call: SyntaxNode, callee: string): number | null {
|
||||
const bare = callee.replace(/\([^()]*\)/g, '');
|
||||
if (!BODY_REPLY.test(bare) || /\b(?:status|code|sendStatus|writeHead)\(/.test(callee)) return null;
|
||||
const receiver = bare.split('.')[0]!;
|
||||
let statement: SyntaxNode | null = call;
|
||||
while (statement.parent && !STATEMENT_BLOCKS.has(statement.parent.type)) statement = statement.parent;
|
||||
const re = new RegExp(`^\\s*(?:await\\s+)?${receiver}\\s*\\.\\s*(?:status|code)\\s*\\(\\s*([1-5]\\d{2})\\s*\\)\\s*;?\\s*$|^\\s*${receiver}\\s*\\.\\s*statusCode\\s*=\\s*([1-5]\\d{2})\\s*;?\\s*$`);
|
||||
let prev: SyntaxNode | null = statement.previousNamedSibling;
|
||||
for (let i = 0; prev && i < STATUS_LOOKBACK; i++, prev = prev.previousNamedSibling) {
|
||||
if (prev.type === 'comment') continue;
|
||||
const m = re.exec(prev.text);
|
||||
if (m) return Number(m[1] ?? m[2]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The text of a call before its arguments, normalised to a member chain. */
|
||||
function calleeChainText(call: SyntaxNode, container: SyntaxNode): string {
|
||||
// Kotlin and Swift wrap the arguments in a `call_suffix`; the callee is
|
||||
|
||||
Reference in New Issue
Block a user