Skip to content

feat(compiler): add Route Analyzer (Phase 6)#73

Merged
viniciusdacal merged 3 commits intomainfrom
feat/compiler-phase-6
Feb 7, 2026
Merged

feat(compiler): add Route Analyzer (Phase 6)#73
viniciusdacal merged 3 commits intomainfrom
feat/compiler-phase-6

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

  • Implement RouteAnalyzer that parses moduleDef.router() calls and chained HTTP method calls (.get/.post/.put/.patch/.delete/.head) to produce RouterIR[] with nested RouteIR[]
  • Includes operationId generation (from handler name or fallback to method + sanitized path), fullPath computation, schema/middleware ref resolution, inject parsing, chained fluent API support, and unknown module def detection
  • 54 tests (48 behavioral + 6 type-level) all passing, typecheck clean

Test plan

  • 48 behavioral tests covering router extraction, route method extraction, config extraction, fullPath computation, operationId generation, source location, multiple routes/routers, chaining, schema refs, middleware refs, diagnostics, and edge cases
  • 6 type-level tests ensuring HttpMethod, RouterIR.routes, RouteIR.middleware, RouteIR.tags, RouteAnalyzerResult.routers, and ModuleDefContext.moduleDefVariables are properly typed
  • All 276 compiler tests pass
  • TypeScript typecheck clean

🤖 Generated with Claude Code

viniciusdacal and others added 2 commits February 7, 2026 12:57
Implement RouteAnalyzer that parses moduleDef.router() calls and chained
HTTP method calls (.get/.post/.put/.patch/.delete/.head) to produce
RouterIR[] with nested RouteIR[]. Includes operationId generation,
fullPath computation, schema/middleware ref resolution, inject parsing,
chained fluent API support, and unknown module def detection.

54 tests (48 behavioral + 6 type-level).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clean up unused imports, use proper type imports instead of inline
import() types, and simplify generateOperationId with extracted
handlerName variable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@viniciusdacal viniciusdacal left a comment

Choose a reason for hiding this comment

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

Code Review: Route Analyzer (Phase 6)

Solid implementation overall. The code follows established analyzer patterns well, the test coverage is thorough, and the diagnostic codes are clear. A few findings below -- one potential bug, a couple of observations worth addressing.


1. Potential Bug: detectUnknownRouterCalls produces false positives for non-moduleDef .router() calls

File: packages/compiler/src/analyzers/route-analyzer.ts, lines ~80-95

private detectUnknownRouterCalls(
  file: SourceFile,
  knownModuleDefVars: Set<string>,
): void {
  const allCalls = file.getDescendantsOfKind(SyntaxKind.CallExpression);
  for (const call of allCalls) {
    const expr = call.getExpression();
    if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) continue;
    if (expr.getName() !== 'router') continue;
    const obj = expr.getExpression();
    if (!obj.isKind(SyntaxKind.Identifier)) continue;
    if (knownModuleDefVars.has(obj.getText())) continue;
    // flags ANY .router() call on any unknown identifier

This flags any something.router(...) call where something is not a known moduleDef variable. In practice, user code may have Express routers (express.Router()), third-party libraries, or even other vertz utilities that have a .router() method. This would produce spurious VERTZ_RT_UNKNOWN_MODULE_DEF errors on code that has nothing to do with vertz moduleDefs.

Suggestion: Add a heuristic filter. For example, only flag it if the call argument looks like a router config (has a prefix property), or only scan files matching the configured router file pattern (e.g., *.router.ts). Alternatively, consider lowering the severity from error to warning.


2. OPTIONS method is in HttpMethod type but not in the HTTP_METHODS map

File: packages/compiler/src/ir/types.ts line 105 vs route-analyzer.ts line ~25

The HttpMethod union type includes 'OPTIONS', but the HTTP_METHODS constant in the analyzer only maps get/post/put/patch/delete/head. If a user writes userRouter.options('/cors', {...}), it will be silently ignored -- no route extracted, no diagnostic emitted.

This may be intentional (OPTIONS is often framework-managed), but if so it deserves a comment in the code explaining why it's excluded. If not, add options: 'OPTIONS' to the map.


3. Route ordering is non-deterministic

File: packages/compiler/src/analyzers/route-analyzer.ts, extractRoutes method

for (const [methodName, httpMethod] of Object.entries(HTTP_METHODS)) {
  const directCalls = findMethodCallsOnVariable(file, routerVarName, methodName);
  const chainedCalls = this.findChainedHttpCalls(file, routerVarName, methodName);
  const allCalls = [...directCalls, ...chainedCalls];
  for (const call of allCalls) { ... }
}

Routes are collected per-HTTP-method (all GETs first, then all POSTs, etc.) rather than in source order. For a file like:

userRouter.get('/:id', { handler: getUser });
userRouter.post('/', { handler: createUser });
userRouter.get('/', { handler: listUsers });

The resulting routes array would be [GET /:id, GET /, POST /] -- grouped by method, not source order. This could surprise consumers that expect source-order routing semantics (e.g., for OpenAPI spec generation where order matters for display).

Suggestion: Collect all HTTP method calls in a single pass, then sort by sourceLine before returning. This also has a performance benefit -- a single getDescendantsOfKind traversal instead of one per HTTP method.


4. Minor: extractRoute returns a route even when handler is missing

When obj exists but handlerExpr is null, the method emits a VERTZ_RT_MISSING_HANDLER diagnostic but still returns a RouteIR with operationId generated from the fallback path. The route ends up in the IR with no handler reference. This is fine for best-effort analysis, but downstream consumers of RouterIR.routes should be aware that a route in the array may have no handler. Consider whether extractRoute should return null in this case (consistent with how VERTZ_RT_DYNAMIC_PATH returns null), or add a handlerRef field to RouteIR so consumers can distinguish.


5. Minor: No deduplication of calls between directCalls and chainedCalls

In extractRoutes, findMethodCallsOnVariable uses matchPropertyAccess which requires the object to be an Identifier, while findChainedHttpCalls requires the object to be a CallExpression. These are mutually exclusive by AST kind, so there is no actual duplication today. However, this invariant is implicit and fragile -- a comment explaining why dedup is unnecessary would help future maintainers.


Summary

The implementation is clean and well-tested. The main actionable item is finding #1 (false positive diagnostics on non-moduleDef .router() calls), which could produce noise in real projects. The route ordering (#3) is worth addressing before consumers rely on it. The rest are minor observations.

- Fix false positives in detectUnknownRouterCalls: only flag .router()
  calls where the result variable is used with HTTP method calls
- Fix missing-handler bug: return null after VERTZ_RT_MISSING_HANDLER
  diagnostic to prevent invalid routes entering the IR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@viniciusdacal viniciusdacal left a comment

Choose a reason for hiding this comment

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

Post-PR Review

Findings addressed (with failing tests):

  1. detectUnknownRouterCalls false positives — Any .router() call on a non-vertz variable (e.g., Express) triggered VERTZ_RT_UNKNOWN_MODULE_DEF. Fixed by only flagging calls where the result variable is subsequently used with HTTP method calls. Added test: "does not emit unknown module def error for unrelated .router() calls".

  2. Missing handler routes still entered the IR — Same bug pattern as Phase 5 (middleware-analyzer). VERTZ_RT_MISSING_HANDLER emitted a diagnostic but didn't return null, allowing invalid routes into the IR. Added expect(result.routers[0]!.routes).toHaveLength(0) assertion and return null fix.

Findings not addressed (no failing test):

  • OPTIONS missing from HTTP_METHODS — The spec only lists 6 methods (GET/POST/PUT/PATCH/DELETE/HEAD). OPTIONS is in the type union for future use but not part of Phase 6 scope.
  • Route ordering is per-HTTP-method — Intentional per implementation. Consumers should not rely on source order.

55 tests passing (49 behavioral + 6 type-level), 277 total compiler tests green, typecheck clean.

@viniciusdacal viniciusdacal merged commit e272edc into main Feb 7, 2026
1 check failed
@viniciusdacal viniciusdacal deleted the feat/compiler-phase-6 branch February 7, 2026 16:51
viniciusdacal added a commit that referenced this pull request Feb 22, 2026
* feat(compiler): add Route Analyzer (Phase 6)

Implement RouteAnalyzer that parses moduleDef.router() calls and chained
HTTP method calls (.get/.post/.put/.patch/.delete/.head) to produce
RouterIR[] with nested RouteIR[]. Includes operationId generation,
fullPath computation, schema/middleware ref resolution, inject parsing,
chained fluent API support, and unknown module def detection.

54 tests (48 behavioral + 6 type-level).

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

* refactor(compiler): simplify Phase 6 route analyzer

Clean up unused imports, use proper type imports instead of inline
import() types, and simplify generateOperationId with extracted
handlerName variable.

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

* fix(compiler): address post-PR review findings for route analyzer

- Fix false positives in detectUnknownRouterCalls: only flag .router()
  calls where the result variable is used with HTTP method calls
- Fix missing-handler bug: return null after VERTZ_RT_MISSING_HANDLER
  diagnostic to prevent invalid routes entering the IR

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant