Skip to content

feat: detect page/websocket kinds across 9 framework extractors#9

Open
jorgeraad wants to merge 14 commits intomainfrom
feat/kind-detection-frameworks
Open

feat: detect page/websocket kinds across 9 framework extractors#9
jorgeraad wants to merge 14 commits intomainfrom
feat/kind-detection-frameworks

Conversation

@jorgeraad
Copy link
Copy Markdown
Collaborator

@jorgeraad jorgeraad commented Apr 29, 2026

Closes #7. Adds page/websocket kind detection across 9 framework extractors (everything in the issue checklist).

Validated against 60 real-world benchmark apps (argus-validation-benchmarks):

  • 53 endpoints reclassified from api → page (Flask render_template, Express res.render, Django TemplateView, etc.)
  • 3 new websocket endpoints detected that the old extractors missed entirely
  • No regressions: same 735 endpoints still detected, plus the 3 new ones (738 total)

Each framework lands as its own commit with a fixture-driven vitest test under src/extractors/fixtures//. Also ports nextjs.test.ts from bun:test to vitest so the suite runs through one runner.


Note

Medium Risk
Touches core route-extraction heuristics across many frameworks, so misclassification or missed endpoints is possible despite added fixture coverage. No auth/data-handling logic is changed, but parsing changes (especially in express.ts and rails.ts) may affect endpoint detection accuracy.

Overview
Adds EndpointInfo.kind classification improvements so extractors can label endpoints as api, page, or websocket (not just defaulting everything to API) across Django, Express, FastAPI, Flask, Go (gin + net/http), Laravel, NestJS, Rails, and Spring.

Implements new heuristics per framework (e.g., template-rendering signals, websocket route registries, and framework-specific constructs like Django Channels websocket_urlpatterns, NestJS @WebSocketGateway/@SubscribeMessage, Spring @MessageMapping, Rails controller inheritance), and tightens route parsing where needed (notably Express route call parsing via balanced-paren scanning).

Introduces a fixture-driven Vitest suite (src/extractors/__fixtures__/** + new *.test.ts files) and wires Vitest into package.json/vitest.config.ts; updates nextjs.test.ts to use Vitest. Tooling is adjusted to ignore/exclude fixtures in ESLint and TypeScript configs.

Reviewed by Cursor Bugbot for commit 712b724. Bugbot is set up for automated code reviews on this repo. Configure here.

Test added 13 commits April 29, 2026 16:02
Sets up vitest as the test runner and adds the first per-extractor
test using the FastAPI fixture (which already emits kind: "websocket"
for @app.websocket routes). Establishes the fixture-driven pattern that
subsequent per-framework tests will follow.
…ls) kinds

Class-based views deriving from TemplateView/ListView/DetailView and FBVs
calling render() are now emitted as kind: "page". websocket_urlpatterns
entries (Django Channels) emit kind: "websocket". Strips .as_view/.as_asgi
from handler names so view classes can be looked up by name.
Replaced the lazy single-line route regex with a balanced-paren walker so
multi-line arrow handler bodies can be inspected. Page detection looks for
res.render(...) or res.sendFile(...) in the handler body. Websocket
detection covers app.ws() (express-ws), io.on('connection')/io.of()
(socket.io), and new WebSocketServer (ws library).
A route is page-shaped when its decorator args set response_class=HTMLResponse
or its function body returns an HTMLResponse(...) or .TemplateResponse(...).
Body window is bounded to the next top-level decorator/def to avoid bleeding
between adjacent routes.
A handler whose body contains render_template() or render_template_string()
emits kind: "page". Body window is bounded to the next top-level def/class/
decorator at column 0 so adjacent handlers do not leak into each other's
classification.
A route handler emits kind: "websocket" when its file imports
github.com/gorilla/websocket and the handler body contains an .Upgrade(
call. Applies uniformly across gin, echo, fiber, and net/http via a shared
findWebsocketHandlers helper. Also tightened the handler-name regex so
trailing handler args resolve correctly.
Routes defined in routes/web.php emit kind: "page" (session/CSRF/Blade
view convention); routes from routes/api.php remain kind: "api". The
file-of-origin distinction is reused from the existing /api prefix logic.
… kinds

Methods carrying @Render('view') emit kind: "page". Classes annotated
with @WebSocketGateway() emit each @SubscribeMessage('event') method as a
websocket endpoint with method WS. Methods are now associated with their
owning class via a per-class scan so controller vs gateway is disambiguated.
Routes are classified by walking the controller's inheritance chain on
disk: ActionController::Base ancestor → kind: "page"; ActionController::API
ancestor → kind: "api". Resolution memoizes per file with a cycle guard.
Also fixes a pre-existing bug where namespace blocks leaked across the
whole routes.rb file — namespaces are now tracked block-by-block.
…g) kinds

Methods on a class annotated with @controller (without @RestController) emit
kind: "page"; @RestController stays kind: "api". @MessageMapping and
@SubscribeMapping methods emit a separate websocket endpoint regardless of
class kind. Class context is resolved via a back-scan from each mapping
match, so inner classes and multi-class files are handled.
Fixture directories under src/extractors/__fixtures__/ contain synthetic
test inputs that mimic external frameworks (e.g. @nestjs/common imports
without the package being installed). They're scanned as raw source text
by the extractors, never compiled or imported. Excluding them from both
the TypeScript program and the ESLint config keeps tooling clean without
needing per-file @ts-nocheck escape hatches.
The rest of the test suite uses vitest (introduced for the per-extractor
fixture tests). Unify on one runner so 'bun run test' covers all suites.
Replaces import.meta.dir (bun-only) with the standard ESM
fileURLToPath(import.meta.url) pattern.
Comment thread src/extractors/nestjs.ts
…r stack

The previous heuristic checked a 400-char backward window and a 400-char
forward window for @Render. In a controller with multiple methods, that
easily included @Render decorators belonging to a sibling method, so a
plain @get sitting near a @Render-decorated handler was misclassified as
a page.

Replace both windows with a precise line-by-line walk that captures only
the contiguous decorator stack of the current method — stopping at any
line that ends a previous statement (}, ;) or otherwise looks like code.
Add a regression test where a @get("/data") sibling sits directly below
a @Render-decorated method and asserts its kind is "api".
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.

Reviewed by Cursor Bugbot for commit 712b724. Configure here.

Comment thread src/extractors/nestjs.ts
// Patch the previous class's `end` to be this class's start
if (classes.length > 0) classes[classes.length - 1]!.end = start;
classes.push({ start, end: content.length, name, decorators });
lastEnd = start;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class decorator attribution includes previous class body

Low Severity

In findClasses, lastEnd = start records the START of each class. For the second class onward, decorators = content.slice(lastEnd, start) captures the entire body of the preceding class (not just the decorators preceding the current class). If a preceding class body contains @Controller("...") or @WebSocketGateway(...) in a string literal, comment, or meta-programming call, the controllerRe / wsGatewayRe exec on c.decorators would falsely match, assigning an incorrect prefix or gateway status to the subsequent class.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 712b724. Configure here.

Comment thread src/extractors/rails.ts
) {
nsStack.pop();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rails depth tracking order wrong for combined lines

Low Severity

The depth tracking processes do (increment) before end (decrement) on the same line. If a line contains both keywords (e.g. end; scope do), the namespace popping logic inside the end handler uses a depth that was already incremented by the do on that same line, causing the wrong namespaces to be popped. Ruby semantics process end (closing) before any new block opening.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 712b724. Configure here.

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.

Extend kind detection to remaining frameworks (pages + websockets)

2 participants