GitNexus/gitnexus/bench/tool-accuracy/expectations.ts
Abhigyan Patwari 565e50276d
fix(routes): resolve Express registrations made through helper parameters (#3629)
* fix(routes): resolve Express registrations made through helper parameters (#3487)

#3603 made Express receivers provenance-proven, which removed the receiver-
name false positives but also every registration on a function parameter:
a parameter has no binding value, so `app.get(...)` inside a helper never
resolved. On GitNexus's own server that dropped `* /api/mcp`
(`mountMCPEndpoints(app, ...)`) and `GET /` (`registerWebUI(app, ...)`),
and the two SSE progress routes registered by
`mountSSEProgress(app, '/api/.../progress', jobs)` were never found at all
(#3487, item a).

Named function values in the route facts now carry an identity and the
binding id of each parameter (a TypeScript `this` parameter takes no
position, and a parameter written anywhere in the body, nested closures
included, takes none either). Calls to plain functions and to namespace or
default import members are recorded as invocations, keeping only the
arguments that can carry an allocation or a literal path; arguments after a
spread have no position. The resolver resolves a registration whose
receiver, literal path or mounted child is a parameter through each call
site of its function, once per site and in the helper's file. This is one
hop only: a site argument that is itself a parameter does not resolve. When
no call site is recorded, the call is evaluated as written, so an unknown
mount path still suppresses its child router instead of exposing its paths
at the root.

Only calls whose callee is named like a function that registers on its
parameters are evaluated, which keeps resolution at its previous cost on
repositories with many helper calls. A function value's invalidation key now
includes its identity, so a property write on one function
(`Button.displayName = ...`) no longer disables every other. The group
layer shares the resolver, so it gets the same routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): forward mounted-child parameters and aliased helper imports

Review follow-ups for the helper-parameter forwarding:

- A helper that forwards only its mounted router (`function mountChild(child)
  { app.use('/api', child) }`) was never resolved, because the forwarded
  function was chosen from the receiver and path alone. Any parameter among
  the receiver and arguments now selects it, as long as all come from one
  function.
- A helper called through an aliased import (`{ mountSSEProgress as
  mountProgress }`) was filtered out before its call sites were recorded,
  since the filter compared only the local name. It now also accepts the
  imported name.
- A write to a same-named local declared in a nested block (`{ let app;
  app = other; }`) discarded the outer parameter. A write to a binding that
  a nested block, function or catch clause declares no longer counts.
- The fixture's `registerWebUI` uses its static directory.

The release tool-accuracy gate now passes `routes.helper-registration`, so
its known-gap allowance is removed. Its expected file moves from the caller
(`api.ts`) to the helper (`progress.ts`): an Express route belongs to the
file of its `app.get` call, which is also where its handler and response
keys are, as for every directly registered route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): treat destructuring reassignment as a parameter write

`[app] = [other]` reassigns `app`, but the write check compared the whole
target text (`[app]`) with the parameter name, so the parameter stayed
forwardable and later registrations used the call-site argument. A
destructuring target now writes each name it binds; a member target still
writes only a property. Also drops an unused local flagged by CodeQL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): follow helpers exported under another name

`export { mountRoutes as setup }` records the export as a binding, so the
call-site filter knew the helper only as `mountRoutes` and skipped callers
importing `setup`. An export of a binding to a named function now adds its
exported name too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-18 22:13:10 +01:00

280 lines
8.4 KiB
TypeScript

/**
* The single place that pins fixture line numbers. run.ts buckets observations by these anchors
* and the fixed answers below are built from them, so a fixture edit is one reviewed change here.
* `anchorDrift` fails loudly (run.ts and a unit test) when a fixture line no longer holds the
* pinned text, instead of letting observations drift into the wrong bucket.
*/
export const FIXTURE_ANCHORS = {
writerCloseDeclaration: { file: 'rename/writer.ts', line: 2, text: 'close(): void {}' },
writerCloseCall: { file: 'rename/writer.ts', line: 9, text: 'writer.close();' },
callerCloseCall: { file: 'rename/caller.ts', line: 4, text: 'writer.close();' },
writerComment: {
file: 'rename/writer.ts',
line: 11,
text: '// close is a comment, not a reference.',
},
writerString: { file: 'rename/writer.ts', line: 12, text: "export const title = 'close';" },
typedWriterFlushDeclaration: {
file: 'rename/typed-writer.ts',
line: 2,
text: 'flush(): void {}',
},
typedWriterFlushCall: {
file: 'rename/typed-writer.ts',
line: 5,
text: 'writer.flush();',
},
runGit: {
file: 'src/security.ts',
line: 4,
text: 'export function runGit(args: string): string {',
},
blameSummary: {
file: 'src/security.ts',
line: 7,
text: 'export function blameSummary(ref: string): string {',
},
handleUnsafe: {
file: 'src/security.ts',
line: 14,
text: 'export function handleUnsafe(req: any): string {',
},
directUnsafe: {
file: 'src/security.ts',
line: 22,
text: 'export function directUnsafe(req: any): void {',
},
directUnsafeSource: {
file: 'src/security.ts',
line: 23,
text: 'const command = req.query.command as string;',
},
directUnsafeSink: { file: 'src/security.ts', line: 24, text: 'execSync(command);' },
} as const satisfies Record<string, { file: string; line: number; text: string }>;
const A = FIXTURE_ANCHORS;
/** Anchors whose fixture line (1-based) no longer holds the pinned text; empty when in sync. */
export function anchorDrift(readLines: (file: string) => string[]): string[] {
return Object.entries(FIXTURE_ANCHORS).flatMap(([name, anchor]) => {
const actual = readLines(anchor.file)[anchor.line - 1]?.trim();
return actual === anchor.text
? []
: [
`${name}: ${anchor.file}:${anchor.line} is ${JSON.stringify(actual)}, expected ${JSON.stringify(anchor.text)}`,
];
});
}
/** Fixed answers reviewed from the fixture source, never learned from tool output. */
export const EXPECTATIONS = [
{
id: 'rename.references',
issue: 3486,
title: 'All resolved Writer.close references',
expected: [
`rename/caller.ts:${A.callerCloseCall.line}:writer.closeWriter();`,
`rename/writer.ts:${A.writerCloseDeclaration.line}:closeWriter(): void {}`,
`rename/writer.ts:${A.writerCloseCall.line}:writer.closeWriter();`,
],
},
{
id: 'rename.comments-strings',
issue: 3486,
title: 'Comments and strings receive no edits',
expected: [],
},
{
id: 'rename.homonyms',
issue: 3486,
title: 'Unrelated methods and functions receive no edits',
expected: [],
},
{
id: 'rename.typed-parameter-refusal',
issue: 3486,
title: 'A structural parameter receiver blocks rename without edits or source changes',
expected: [
'status=error planning=unsupported application=not_started code=ambiguous_reference',
'edits=0 semantic=0 graph=0 text_search=0 files=0 applied=false changes=0',
'sources=unchanged',
],
},
{
id: 'routes.production-handler',
issue: 3487,
title: 'Test stub cannot replace the server handler',
expected: ['GET /api/info src/server/api.ts'],
},
{
id: 'routes.all-method',
issue: 3487,
title: 'Express app.all is method agnostic',
expected: ['* /api/mcp src/server/api.ts'],
},
{ id: 'routes.map-lookup', issue: 3487, title: 'Map.get is not an HTTP route', expected: [] },
{
id: 'routes.helper-registration',
issue: 3487,
title: 'Literal route forwarded through a local helper',
// Like every Express route, it belongs to the file of its `app.get` call,
// which is also where its handler and response keys are.
expected: ['GET /api/progress src/server/progress.ts'],
},
{
id: 'api.template-wrapper',
issue: 3488,
title: 'Base URL template passed to a fetch wrapper is a consumer',
expected: ['src/web/client.ts'],
},
{
id: 'api.literal-control',
issue: 3488,
title: 'Direct literal fetch consumer control',
expected: ['src/web/control.ts'],
},
{
id: 'explain.named-flow',
issue: 3489,
title: 'Named request handler reaches the command sink',
expected: ['handleUnsafe -> runGit command-injection'],
},
{
id: 'explain.inline-flow',
issue: 3489,
title: 'Inline request callback reaches the command sink',
expected: ['src/server/inline.ts -> runGit command-injection'],
},
{
id: 'explain.guard',
issue: 3490,
title: 'Strict SHA allow-list prevents command injection',
expected: [],
},
{
id: 'explain.constant-control',
issue: 3490,
title: 'Constant command has no request taint',
expected: [],
},
{
id: 'explain.cross-lines',
issue: 3491,
title: 'Cross-function source, sink and every hop are 1-based',
expected: [
`source handleUnsafe@${A.handleUnsafe.line}`,
`sink runGit@${A.runGit.line}`,
`hop 0 handleUnsafe@${A.handleUnsafe.line}`,
`hop 1 blameSummary@${A.blameSummary.line}`,
`hop 2 runGit@${A.runGit.line}`,
],
},
{
id: 'explain.intra-lines',
issue: 3491,
title: 'Statement-level source, sink and hops are 1-based',
expected: [
`function@${A.directUnsafe.line}`,
`source command@${A.directUnsafeSource.line}`,
`sink@${A.directUnsafeSink.line}`,
`hop 0 command@${A.directUnsafeSource.line}`,
`hop 1 command@${A.directUnsafeSink.line}`,
],
},
{
id: 'python.module-direct',
issue: 3497,
title: 'Direct imported-module receiver control',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.module-chain',
issue: 3497,
title: 'Module-return helper chained receiver',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.module-local',
issue: 3497,
title: 'Module-return helper assigned to a local',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.factory-direct',
issue: 3498,
title: 'Annotated factory direct assignment control',
expected: ['mod.py:C.m'],
},
{
id: 'python.factory-conditional',
issue: 3498,
title: 'Annotated factory through conditional expression',
expected: ['mod.py:C.m'],
},
{
id: 'python.factory-qualified',
issue: 3498,
title: 'Annotated factory through module alias',
expected: ['mod.py:C.m'],
},
{
id: 'python.factory-boolean',
issue: 3498,
title: 'Annotated factory through boolean expression',
expected: ['mod.py:C.m'],
},
{
id: 'python.nested-local-import',
issue: 3499,
title: 'Function-local import beats unrelated nested declaration',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.nested-module-import',
issue: 3499,
title: 'Module import beats unrelated nested declaration',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.nested-unbound',
issue: 3499,
title: 'Sibling cannot call an out-of-scope nested declaration',
expected: [],
},
{
id: 'python.local-import-control',
issue: 3499,
title: 'Function-local import without homonym control',
expected: ['pkg/facade.py:target'],
},
{
id: 'python.local-import-leak',
issue: 3499,
title: 'Function-local import cannot bind a sibling function',
expected: [],
},
{
id: 'summary.exact-control',
issue: 3497,
title: 'Complete empty drop summary permits exact',
expected: ['exact'],
},
{
id: 'summary.retained-lower-bound',
issue: 3497,
title: 'Retained dropped name requires lower-bound',
expected: ['lower-bound'],
},
{
id: 'summary.cap',
issue: 3497,
title: '501 names over 1001 sites retain 500 and explicitly omit the last name',
expected: ['kept=500 total=1001 omitted=1 target=false'],
},
{
id: 'summary.omitted-lower-bound',
issue: 3497,
title: 'Absent name in a truncated summary cannot claim exact',
expected: ['lower-bound'],
},
] satisfies Array<{ id: string; issue: number; title: string; expected: string[] }>;