Skip to content

fix: name a route after its own decorator, not the file's first - #61

Merged
TheCrazyAnt merged 1 commit into
mainfrom
claude/new-issuer-review-24c0eb
Sep 3, 2026
Merged

TheCrazyAnt merged 1 commit into
mainfrom
claude/new-issuer-review-24c0eb

Conversation

@TheCrazyAnt

Copy link
Copy Markdown
Owner

Closes #60.

What was wrong

routeFromDecorators resolved a route's path with file.calls.find((item) => item.callee === decorator && item.stringArguments.length). file.calls is the flat call list for the whole file, so every @app.post in a module resolved to the first app.post(...) in that file.

Reproduced before touching anything, with the module from #60:

GET /health              <- app.py#health
POST /api/first          <- app.py#first_handler
POST /api/first          <- app.py#second_handler   ← should be /api/second
POST /api/first          <- app.py#third_handler    ← should be /api/third

file:line, symbol and edges were all correct — only name and metadata.path were wrong, which is enough to make documentedCapabilityLabel match on the wrong path and collapse a single-module app's feature list.

One route per file hid it: the file-wide lookup and the per-declaration lookup return the same call, and no fixture put two same-method routes in one module.

The fix

extract.py already emits everything needed — it visits decorator_list inside the function scope, so each decorator call carries enclosingFunction set to the decorated function and a line just above the def. This is a TypeScript-side change only.

A route now reads the nearest matching call above its own declaration:

function decoratorCall(decorator: string, declaration: PythonFunction, file: PythonFile): PythonCall | undefined {
  let nearest: PythonCall | undefined;
  for (const item of file.calls) {
    if (item.callee !== decorator) continue;
    if (item.stringArguments.length === 0) continue;
    if (item.enclosingFunction !== declaration.name) continue;
    if (item.line > declaration.line) continue;
    if (!nearest || item.line > nearest.line) nearest = item;
  }
  return nearest;
}

Why enclosingClass is not compared

The patch suggested in #60 also required item.enclosingClass === declaration.enclosingClass. That breaks a route declared inside a class method: the extractor records the class on the call but not on the nested declaration, so the two never match and the path degrades to /.

fn   nested_handler  line 9  enclosingClass None
call app.get         line 8  encFn nested_handler  encClass Registrar

The nearest-above rule already separates same-named methods in different classes, so the class comparison only loses information.

Evidence

Red before green. The two new tests were written first and run against the unmodified adapter: exactly those 2 failed, the existing 7 passed. After the fix: 9 passed.

Mutation proof. Each plausible wrong implementation was applied in turn and had to turn a test red (implementation restored byte-identical afterwards):

Mutation Result
A — file-wide first match (the original bug) 2 red
B — no line filter 1 red
C — first match above instead of nearest 1 red
D — also require enclosingClass to match 1 red
E — drop the string-argument filter green; equivalent to the shipped implementation on realistic input, not a gap

The first draft of the tests did not catch D — the nested case was inside a plain function rather than a class method. The class Wiring case was added for it.

Full npm run check: typecheck passes, 24 files / 213 tests pass (211 before, 2 new), build passes.

TypeScript adapter checked too: it reads the path off the call expression it is iterating (adapters/typescript/src/index.ts:911), so it resolves per call site and does not share this bug. No change there.

🤖 Generated with Claude Code

The Python adapter looked a route's path up with `file.calls.find` by the
decorator's dotted name, so every `@app.post` in a module resolved to the
first one. A real Flask module with 57 routes produced 55 nodes carrying 4
distinct names, and `documentedCapabilityLabel` then matched 18 unrelated
handlers to one capability. One route per file hid it: the file-wide lookup
and the per-declaration lookup return the same call.

The extractor already records each decorator call inside the scope of the
function it decorates, on a line above the `def`, so a route now reads the
nearest such call above its own declaration. `enclosingClass` is not part of
the match: a route declared inside a method carries the class on the call but
not on the declaration, so comparing the two loses the path instead of
sharpening it, while the nearest-above rule already separates same-named
methods in different classes.

The existing fixtures could not catch this, so the two new tests cover two
same-method routes in one module and the scope cases that pin the tie-break.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheCrazyAnt
TheCrazyAnt merged commit 90a1714 into main Sep 3, 2026
8 checks passed
@TheCrazyAnt TheCrazyAnt mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Python adapter labels every same-method route in a file with the first decorator's path

1 participant