Skip to content

feat(#72): SPI v1 slice 3b-ii — NestJS QoL (guards/pipes/injects)#99

Merged
mohanagy merged 2 commits into
mainfrom
feat/spi-v1-slice-3b-ii-nestjs-qol
May 10, 2026
Merged

feat(#72): SPI v1 slice 3b-ii — NestJS QoL (guards/pipes/injects)#99
mohanagy merged 2 commits into
mainfrom
feat/spi-v1-slice-3b-ii-nestjs-qol

Conversation

@mohanagy
Copy link
Copy Markdown
Owner

@mohanagy mohanagy commented May 10, 2026

Stacked on #98. Targets `feat/spi-v1-slice-3b-nestjs-base` so reviewers see only the 3b-ii diff. GitHub will auto-rebase against `main` on merge once #98 lands.

Summary

  • Layers the NestJS quality-of-life additions on top of feat(#72): SPI v1 slice 3b — base NestJS framework layer #98's slice 3b base, completing the framework-decorator surface called for in the SPI v1 design.
  • Emits `injects` edges from constructor parameters typed as classes (high confidence) and from `@Inject('TOKEN')` parameters resolved through a workspace-wide token map (medium confidence).
  • Emits `guards` / `intercepts` / `pipes` edges for both class-level (`@UseGuards` on the controller) and method-level (`@UseGuards` on a route handler) decorator placements; supports both positional (`@UseGuards(A, B)`) and array (`@UseGuards([A, B])`) argument forms.
  • Resolves dynamic `Module.forRoot(...)` / `forRootAsync(...)` / `forFeature(...)` / `forFeatureAsync(...)` / `register(...)` / `registerAsync(...)` to a low-confidence `module_imports` edge to the receiver Module class plus a specific `spi.nest.module-metadata.dynamic` diagnostic. (Slice 3b base previously emitted no edge here; the existing test was updated.)

Token-map design

A workspace-level pre-pass walks every `@Module` decorator before per-file framework detection, building a single `Map<token, { classSymbolId, confidence }>`. Only `useClass` / `useExisting` bindings register; `useValue` and `useFactory` providers are intentionally absent because they have no well-defined class target. `@Inject('TOKEN')` against a value-only or factory-only or unknown token produces an info-level `spi.nest.inject-token.unresolved` diagnostic instead of a spurious edge.

If two modules bind the same token to different classes, the last one wins (token uniqueness is the user's responsibility).

Diagnostic surface

Every unresolvable case produces an info-level `SpiDiagnostic` so coverage gaps stay auditable:

  • `spi.nest.module-metadata.dynamic` — `Module.forRoot`-style entries
  • `spi.nest.module-metadata.unresolved` — spread, conditional, computed array entries
  • `spi.nest.module-metadata.non-array` — `imports: someComputedArray`
  • `spi.nest.use-decorator.unresolved` — `@UseGuards(new Foo())` and similar non-class arguments
  • `spi.nest.inject-token.unresolved` — `@Inject('TOKEN')` with no matching binding

Test plan

  • `npm run typecheck` — clean
  • `npm run build` — clean
  • `npm run test:run` — 84 files / 1505 tests pass (15 new for 3b-ii on top of 17 from 3b base)
  • New tests cover: constructor type-resolved injects (incl. provider-on-provider injection, dedupe, no-edge for unresolvable types); `@Inject('TOKEN')` resolution against `useClass` and `useExisting` (medium confidence); diagnostics for unbound tokens and `useValue`-only bindings; class-level + method-level `@UseGuards` / `@UseInterceptors` / `@UsePipes`; positional and array argument forms; `@UseGuards(new Foo())` diagnostic; `Module.forRoot()` and `forRootAsync()` low-confidence edges + diagnostics; demo-repo regression check now also asserts zero `guards` / `pipes` / `intercepts` / `injects` edges
  • CI must pass on Ubuntu/macOS/Windows matrix before merge

What's next

Refs #70 (design), #72 (implementation umbrella), #98 (parent slice 3b base).

Summary by CodeRabbit

Release Notes

  • New Features
    • Improved NestJS framework analysis with enhanced injection token binding resolution.
    • Added workspace-level detection of provider dependencies and dynamic module patterns.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

This PR adds a workspace-level token-binding pre-pass to NestJS framework detection. The SPI builder now collects provider tokens upfront, threads the token map through per-file framework detection, and emits constructor injection edges, @Use* decorator edges, and resolves @Inject('TOKEN') parameters with appropriate confidence levels.

Changes

NestJS Token Binding & Decorator Framework

Layer / File(s) Summary
Token Binding Data Model
src/pipeline/spi/framework-nestjs.ts
NestBindings expanded to track useGuards, useInterceptors, usePipes, and inject decorators. New exported types: NestTokenBinding, NestTokenMap, CollectNestTokenMapOptions. DetectNestFrameworkContext adds tokenMap field. DYNAMIC_MODULE_FACTORY_METHODS set added for dynamic module shape recognition.
Workspace Token Collection
src/pipeline/spi/framework-nestjs.ts
New exported collectNestTokenMap() function walks source files, extracts providers entries with useClass/useExisting, and builds token map with per-token confidence. Helper utilities added: providerArrayFor, stringPropertyValue, identifierPropertyValue.
Decorator Binding Detection
src/pipeline/spi/framework-nestjs.ts
collectNestBindings() extended to detect @UseGuards, @UseInterceptors, @UsePipes, @Inject decorators alongside existing bindings.
Constructor Injection & Token Resolution
src/pipeline/spi/framework-nestjs.ts
emitConstructorInjects() updated to resolve @Inject('TOKEN') via ctx.tokenMap (medium confidence), emit info-level diagnostic for unresolved tokens, and fall back to type-node resolution (high confidence). Dedupes emitted injection edges per class/target. Adds injectTokenFromParameter() and resolveTypeNodeToClass() helpers.
Static Class Reference Resolution
src/pipeline/spi/framework-nestjs.ts
resolveStaticClassReference() refactored through new resolveStaticClassFromIdentifier() helper to follow aliases, filter declaration-file symbols, and map declaration source files to fileId.
Module Metadata & Dynamic Modules
src/pipeline/spi/framework-nestjs.ts
emitModuleMetadataEdge() reworked to: emit high-confidence edges for static class references, emit low-confidence edges for dynamic module factory expressions (e.g., forRoot/forRootAsync) with diagnostic, record unresolved diagnostic for computed/unresolved entries.
Controller Routes & Use Decorators
src/pipeline/spi/framework-nestjs.ts
Controller route detection updated to skip non-route methods, emit route edges, then emit method-level guards/intercepts/pipes edges. New emitUseEdgesFor() helper resolves @Use* arguments to static class references with high confidence, emits info-level diagnostics for unresolved targets. Route decorator lookup refactored via helper.
Framework Detector Control Flow
src/pipeline/spi/framework-nestjs.ts
detectNestFramework() control flow changed to emit constructor injection for nest_module, nest_controller, nest_provider; controllers now emit class-level use edges before route discovery.
Builder Integration
src/pipeline/spi/build.ts
Documentation updated to describe slice 3b-ii pre-pass. collectNestTokenMap imported. Default extractorVersion bumped from spi-v1.0.0-slice-3b to spi-v1.0.0-slice-3b-ii. Workspace token-binding pass added in addTypeCheckerEdges: collects TypeScript SourceFiles, builds tokenMap via collectNestTokenMap(), passes into detectNestFramework() calls.
Public Exports
src/pipeline/spi/index.ts
Re-exports collectNestTokenMap, CollectNestTokenMapOptions, NestTokenMap, NestTokenBinding from framework-nestjs module.
Test Coverage
tests/unit/spi-framework-nestjs.test.ts
Updated dynamic Module.forRoot() and Module.forRootAsync() tests to expect low-confidence module_imports edges with diagnostics. Demo-repo test framework kinds list extended. New slice 3b-ii test suite: constructor injects with deduping, @Inject('TOKEN') resolution (medium confidence), unresolved token diagnostics, @UseGuards/@UseInterceptors/@UsePipes class-level and method-level edges, and unresolvable decorator target diagnostics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mohanagy/graphify-ts#98: Introduces the core NestJS framework detection that this PR directly extends with token-binding pre-pass and constructor/decorator-derived edge emission.
  • mohanagy/graphify-ts#90: Adds workspace-level TypeScript type-checker pass (ts.Program) that provides the foundation for the collectNestTokenMap pre-pass added here.
  • mohanagy/graphify-ts#88: Introduces the SPI file-layer builder and exports; this PR extends buildSpi to add workspace token-binding and bump extractorVersion.

Poem

🐰 Tokens dance in bundles bright,
Injections flow through NestJS night,
Guards and pipes in formation stand,
A decorator symphony, hand in hand!
Whish!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: slice 3b-ii NestJS quality-of-life additions including guards, pipes, and injects edges.
Description check ✅ Passed The description comprehensively covers all template sections: detailed summary of changes, explicit test plan results, checklist items marked, and related issues referenced.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spi-v1-slice-3b-ii-nestjs-qol

Comment @coderabbitai help to get the list of available commands and usage tips.

@mohanagy mohanagy changed the base branch from feat/spi-v1-slice-3b-nestjs-base to main May 10, 2026 17:35
mohanagy added 2 commits May 10, 2026 21:36
Stacks the NestJS quality-of-life additions on top of slice 3b base:

  * `injects` edges from the class symbol of any nest_module / nest_
    controller / nest_provider class. Constructor parameters typed as a
    class resolve through ts.TypeChecker (high confidence).
  * `injects` edges sourced from `@Inject('TOKEN')` parameter decorators.
    A workspace-wide token map is built once before the per-file pass
    from every `@Module` provider list's `useClass` / `useExisting`
    bindings; tokens with `useValue` / `useFactory` (no class target)
    are intentionally absent and produce a diagnostic when injected.
    Resolved string-token injections emit medium-confidence edges.
  * `guards` / `intercepts` / `pipes` edges from the matching
    `@UseGuards` / `@UseInterceptors` / `@UsePipes` decorators. Class-
    level decorators emit from the controller class symbol (apply to
    all routes); method-level decorators emit from the route method
    symbol. Both positional `@UseGuards(A, B)` and array
    `@UseGuards([A, B])` forms are supported.
  * Dynamic Module shapes — `Module.forRoot(...)` / `forRootAsync(...)`
    / `forFeature(...)` / `forFeatureAsync(...)` / `register(...)` /
    `registerAsync(...)` — now resolve the receiver class and emit a
    low-confidence module_imports edge plus a specific
    `spi.nest.module-metadata.dynamic` diagnostic recording that the
    runtime providers list could not be enumerated. The 3b base test
    that asserted no edge for forRoot is updated to expect the new
    low-confidence behavior.

Every gap that cannot be resolved (untyped parameters, value-only
tokens, instance-literal `@UseGuards(new Foo())`, computed metadata
arrays) emits an info-level SpiDiagnostic so downstream consumers can
audit coverage instead of silently missing edges.

Default `extractor_version` bumps to `spi-v1.0.0-slice-3b-ii`. The
framework-decorator pass still piggybacks on the existing ts.Program
created for the call/type layers, plus one workspace-level pre-scan
to gather token bindings.
The slice 3b-ii rewrite of framework-nestjs.ts inherited the same
unreachable inner-`if` branch CodeRabbit flagged on 3b base (#98). The
fix is identical: drop the dead block, simplify the loop so any
identifier-named method declaration unconditionally bumps the
overload counter before the route-decorator check decides whether to
emit an edge.

Mirrors the regression test from 3b base so 3b-ii independently
verifies that the controller_route edge targets the implementation's
`#2` overload id when the route method is overloaded.

When 3b base lands on main, this commit's framework-nestjs.ts fix
becomes a no-op against the rebased base; the test addition still
applies cleanly because 3b-ii already owns this test file.
@mohanagy mohanagy force-pushed the feat/spi-v1-slice-3b-ii-nestjs-qol branch from 183294f to 2cb1a29 Compare May 10, 2026 17:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pipeline/spi/framework-nestjs.ts`:
- Around line 126-140: collectNestTokenMap currently relies on the incoming
opts.sourceFiles order so "last binding wins" is non-deterministic across
platforms; make the resolution deterministic by normalizing traversal order
before registering providers (e.g., sort opts.sourceFiles by a stable key such
as sourceFile.fileName) or by collecting all provider entries first and then
resolving duplicates with a deterministic rule; update collectNestTokenMap to
iterate a sorted list (using sourceFile.fileName) or to deduplicate via a stable
sort/merge step before calling registerProviderTokens, referencing
collectNestTokenMap, registerProviderTokens, collectNestBindings, decoratorsOf,
and decoratorIdentifierName to locate the relevant logic.
- Around line 375-388: resolveDynamicModuleReceiver currently only accepts a
bare identifier receiver and skips namespace-qualified receivers like
Nest.TypeOrmModule.forRoot(); update it to accept ts.PropertyAccessExpression
receivers by extracting the rightmost identifier (e.g., receiver.name for a
PropertyAccessExpression, and walking nested PropertyAccessExpressions if
needed) and pass that identifier to resolveStaticClassFromIdentifier (keep using
ctx.checker and ctx.pathToFileId). Ensure you still verify the extracted token
is an Identifier before calling resolveStaticClassFromIdentifier and return the
same { classSymbolId, factoryName } shape when found.
- Around line 537-538: The current code uses classDecl.members.find(...) to grab
a constructor which may pick an overload signature instead of the
implementation; change the selection of ctor (the constructor declaration) to
explicitly choose the implementation constructor (the one with a body) rather
than the first match—e.g., filter members by ts.isConstructorDeclaration and
then pick the one with a .body (or iterate members in reverse to find the first
constructor with a body) before reading injected params so injected edges are
derived from the real implementation constructor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb5fff7e-e609-425a-a23b-91fb17a9addc

📥 Commits

Reviewing files that changed from the base of the PR and between 2882990 and 2cb1a29.

📒 Files selected for processing (4)
  • src/pipeline/spi/build.ts
  • src/pipeline/spi/framework-nestjs.ts
  • src/pipeline/spi/index.ts
  • tests/unit/spi-framework-nestjs.test.ts

Comment on lines +126 to +140
export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap {
const tokens: NestTokenMap = new Map()
for (const sourceFile of opts.sourceFiles) {
const bindings = collectNestBindings(sourceFile)
if (bindings.module.size === 0) continue
for (const stmt of sourceFile.statements) {
if (!ts.isClassDeclaration(stmt) || !stmt.name) continue
for (const decorator of decoratorsOf(stmt)) {
const name = decoratorIdentifierName(decorator)
if (!name || !bindings.module.has(name)) continue
registerProviderTokens(decorator, opts, tokens)
}
}
}
return tokens
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stabilize duplicate-token resolution across platforms.

collectNestTokenMap() says "last binding wins", but today that winner depends on the incoming sourceFiles order. In the builder, that order comes from recursive directory traversal, so duplicate provide keys can resolve to different classes on Ubuntu/macOS/Windows and make injects edges nondeterministic.

Suggested fix
 export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap {
   const tokens: NestTokenMap = new Map()
-  for (const sourceFile of opts.sourceFiles) {
+  const sourceFiles = [...opts.sourceFiles].sort((a, b) => a.fileName.localeCompare(b.fileName))
+  for (const sourceFile of sourceFiles) {
     const bindings = collectNestBindings(sourceFile)
     if (bindings.module.size === 0) continue
     for (const stmt of sourceFile.statements) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap {
const tokens: NestTokenMap = new Map()
for (const sourceFile of opts.sourceFiles) {
const bindings = collectNestBindings(sourceFile)
if (bindings.module.size === 0) continue
for (const stmt of sourceFile.statements) {
if (!ts.isClassDeclaration(stmt) || !stmt.name) continue
for (const decorator of decoratorsOf(stmt)) {
const name = decoratorIdentifierName(decorator)
if (!name || !bindings.module.has(name)) continue
registerProviderTokens(decorator, opts, tokens)
}
}
}
return tokens
export function collectNestTokenMap(opts: CollectNestTokenMapOptions): NestTokenMap {
const tokens: NestTokenMap = new Map()
const sourceFiles = [...opts.sourceFiles].sort((a, b) => a.fileName.localeCompare(b.fileName))
for (const sourceFile of sourceFiles) {
const bindings = collectNestBindings(sourceFile)
if (bindings.module.size === 0) continue
for (const stmt of sourceFile.statements) {
if (!ts.isClassDeclaration(stmt) || !stmt.name) continue
for (const decorator of decoratorsOf(stmt)) {
const name = decoratorIdentifierName(decorator)
if (!name || !bindings.module.has(name)) continue
registerProviderTokens(decorator, opts, tokens)
}
}
}
return tokens
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/spi/framework-nestjs.ts` around lines 126 - 140,
collectNestTokenMap currently relies on the incoming opts.sourceFiles order so
"last binding wins" is non-deterministic across platforms; make the resolution
deterministic by normalizing traversal order before registering providers (e.g.,
sort opts.sourceFiles by a stable key such as sourceFile.fileName) or by
collecting all provider entries first and then resolving duplicates with a
deterministic rule; update collectNestTokenMap to iterate a sorted list (using
sourceFile.fileName) or to deduplicate via a stable sort/merge step before
calling registerProviderTokens, referencing collectNestTokenMap,
registerProviderTokens, collectNestBindings, decoratorsOf, and
decoratorIdentifierName to locate the relevant logic.

Comment on lines +375 to +388
function resolveDynamicModuleReceiver(
expr: ts.Expression,
ctx: DetectNestFrameworkContext,
): { classSymbolId: string; factoryName: string } | null {
if (!ts.isCallExpression(expr)) return null
const callee = expr.expression
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return null
const factoryName = callee.name.text
if (!DYNAMIC_MODULE_FACTORY_METHODS.has(factoryName)) return null
const receiver = callee.expression
if (!ts.isIdentifier(receiver)) return null
const id = resolveStaticClassFromIdentifier(receiver, ctx.checker, ctx.pathToFileId)
if (!id) return null
return { classSymbolId: id, factoryName }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '375,388p'

Repository: mohanagy/graphify-ts

Length of output: 832


🏁 Script executed:

rg -n "resolveStaticClassReference" src/pipeline/spi/framework-nestjs.ts

Repository: mohanagy/graphify-ts

Length of output: 245


🏁 Script executed:

rg -n "resolveStaticClassFromIdentifier" src/pipeline/spi/framework-nestjs.ts

Repository: mohanagy/graphify-ts

Length of output: 470


🏁 Script executed:

rg -n "function resolve|const resolve" src/pipeline/spi/framework-nestjs.ts | head -20

Repository: mohanagy/graphify-ts

Length of output: 305


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '618,632p'

Repository: mohanagy/graphify-ts

Length of output: 595


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '633,650p'

Repository: mohanagy/graphify-ts

Length of output: 945


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '325,330p'

Repository: mohanagy/graphify-ts

Length of output: 317


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '489,495p'

Repository: mohanagy/graphify-ts

Length of output: 384


Handle namespace-qualified dynamic module receivers too.

This only resolves SomeModule.forRoot(). A namespace import like Nest.TypeOrmModule.forRoot() is still statically recoverable, but this path drops the new low-confidence edge because it requires the receiver to be a bare identifier.

Suggested fix
 function resolveDynamicModuleReceiver(
   expr: ts.Expression,
   ctx: DetectNestFrameworkContext,
 ): { classSymbolId: string; factoryName: string } | null {
   if (!ts.isCallExpression(expr)) return null
   const callee = expr.expression
   if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return null
   const factoryName = callee.name.text
   if (!DYNAMIC_MODULE_FACTORY_METHODS.has(factoryName)) return null
-  const receiver = callee.expression
-  if (!ts.isIdentifier(receiver)) return null
-  const id = resolveStaticClassFromIdentifier(receiver, ctx.checker, ctx.pathToFileId)
+  const id = resolveStaticClassReference(callee.expression, ctx)
   if (!id) return null
   return { classSymbolId: id, factoryName }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/spi/framework-nestjs.ts` around lines 375 - 388,
resolveDynamicModuleReceiver currently only accepts a bare identifier receiver
and skips namespace-qualified receivers like Nest.TypeOrmModule.forRoot();
update it to accept ts.PropertyAccessExpression receivers by extracting the
rightmost identifier (e.g., receiver.name for a PropertyAccessExpression, and
walking nested PropertyAccessExpressions if needed) and pass that identifier to
resolveStaticClassFromIdentifier (keep using ctx.checker and ctx.pathToFileId).
Ensure you still verify the extracted token is an Identifier before calling
resolveStaticClassFromIdentifier and return the same { classSymbolId,
factoryName } shape when found.

Comment on lines +537 to +538
const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
if (!ctor) return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -name "framework-nestjs.ts" -type f

Repository: mohanagy/graphify-ts

Length of output: 120


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '520,560p'

Repository: mohanagy/graphify-ts

Length of output: 1552


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '530,550p'

Repository: mohanagy/graphify-ts

Length of output: 941


🏁 Script executed:

cat package.json | grep -A 2 -B 2 '"typescript"'

Repository: mohanagy/graphify-ts

Length of output: 307


🏁 Script executed:

# Check if there are any test files or examples showing constructor overloads
grep -r "constructor.*overload\|constructor.*signature" . --include="*.ts" --include="*.js" -l | head -5

Repository: mohanagy/graphify-ts

Length of output: 46


🏁 Script executed:

cat -n src/pipeline/spi/framework-nestjs.ts | sed -n '500,600p'

Repository: mohanagy/graphify-ts

Length of output: 4142


🏁 Script executed:

find . -name "tsconfig.json" -o -name "tsconfig*.json" | head -3

Repository: mohanagy/graphify-ts

Length of output: 137


🏁 Script executed:

cat tsconfig.json | grep -A 5 -B 5 "target\|lib"

Repository: mohanagy/graphify-ts

Length of output: 365


🏁 Script executed:

# Check if findLast is used anywhere in the codebase
grep -r "findLast" . --include="*.ts" --include="*.js" | head -5

Repository: mohanagy/graphify-ts

Length of output: 375


🏁 Script executed:

# Verify TypeScript constructor overload behavior and check for test cases
grep -r "constructor" src/pipeline/spi/framework-nestjs.ts -A 2 -B 2

Repository: mohanagy/graphify-ts

Length of output: 671


🏁 Script executed:

# Look for any test files related to this function
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "emitConstructorInjects\|framework-nestjs" 2>/dev/null | head -3

Repository: mohanagy/graphify-ts

Length of output: 46


🏁 Script executed:

# Check the broader context to understand how parameter decorators work
grep -n "decoratorsOf\|injectTokenFromParameter" src/pipeline/spi/framework-nestjs.ts | head -10

Repository: mohanagy/graphify-ts

Length of output: 525


🏁 Script executed:

# Check if there's a helper or polyfill for findLast
grep -r "findLast\|Array.prototype.findLast" . --include="*.ts" --include="*.js" -B 2 -A 2

Repository: mohanagy/graphify-ts

Length of output: 826


🏁 Script executed:

# Look for any usages of constructor with multiple signatures in test data or examples
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec grep -l "constructor.*\|.*constructor" {} \; | xargs grep -l "@Injectable\|@Controller\|@Module" | head -3

Repository: mohanagy/graphify-ts

Length of output: 184


🏁 Script executed:

# Check the TypeScript AST behavior - look at how constructors are handled elsewhere in the codebase
grep -n "ts.isConstructorDeclaration\|ConstructorDeclaration" src/pipeline/spi/framework-nestjs.ts -A 3 -B 1

Repository: mohanagy/graphify-ts

Length of output: 257


🏁 Script executed:

# Verify if there are any examples or tests showing constructor overloads
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.fixture.ts" \) -exec grep -l "constructor" {} \; | head -5

Repository: mohanagy/graphify-ts

Length of output: 299


🏁 Script executed:

# Check if the code handles methods similarly - do they have the same issue?
grep -n "\.members\.find" src/pipeline/spi/framework-nestjs.ts -B 3 -A 3

Repository: mohanagy/graphify-ts

Length of output: 325


🏁 Script executed:

# Check the test file for constructor-related tests
cat tests/unit/spi-framework-nestjs.test.ts | head -100

Repository: mohanagy/graphify-ts

Length of output: 3363


🏁 Script executed:

# Search for constructor overload examples in test files
grep -n "constructor" tests/unit/spi-framework-nestjs.test.ts -A 5 -B 2 | head -50

Repository: mohanagy/graphify-ts

Length of output: 1892


🏁 Script executed:

# Check if Array.prototype.findLast is available in ES2022
# Let's see if there are any polyfills or compatibility checks
grep -r "ES2022\|ES2023\|findLast" . --include="*.json" --include="*.ts" | grep -v node_modules | head -20

Repository: mohanagy/graphify-ts

Length of output: 513


🏁 Script executed:

# Look at the fixture file to understand real test scenarios
cat tests/fixtures/nest-auth.controller.ts

Repository: mohanagy/graphify-ts

Length of output: 831


🏁 Script executed:

# Check if there are multiple constructors in any TypeScript file in the codebase
find . -type f -name "*.ts" ! -path "*/node_modules/*" -exec grep -l "constructor.*constructor" {} \; | head -5

Repository: mohanagy/graphify-ts

Length of output: 124


🏁 Script executed:

# Let's check if there are any constructor overload examples in TypeScript files
find . -type f -name "*.ts" ! -path "*/node_modules/*" -exec grep -l "constructor(.*)" {} \; | xargs grep -B 2 "constructor(.*)" | grep -A 2 "constructor(.*)" | head -30

Repository: mohanagy/graphify-ts

Length of output: 1295


🏁 Script executed:

# Verify Array.prototype.findLast availability - check TypeScript lib definitions
grep -r "findLast" . --include="*.d.ts" 2>/dev/null | head -5

Repository: mohanagy/graphify-ts

Length of output: 46


🏁 Script executed:

# Look for any existing polyfills or Array extensions
grep -r "findLast\|Array\.prototype" src/ --include="*.ts" | grep -v "\.test\." | head -10

Repository: mohanagy/graphify-ts

Length of output: 371


🏁 Script executed:

# Check the actual behavior - can we use an alternative to findLast that works in ES2022?
# Let's look at what methods are used with .members in the codebase
grep -n "\.members\." src/pipeline/spi/framework-nestjs.ts | head -20

Repository: mohanagy/graphify-ts

Length of output: 177


🏁 Script executed:

# Let's check if there are actual constructor overload examples in real code
grep -r "constructor(" tests/ examples/ --include="*.ts" | grep -B 5 "constructor(" | head -40

Repository: mohanagy/graphify-ts

Length of output: 2002


🏁 Script executed:

# Verify the logic: does ctor.parameters only exist on implementation?
# Let's create a minimal test to understand TypeScript behavior
cat > /tmp/test_constructor.ts << 'EOF'
class Test {
  constructor(x: number)
  constructor(y: string)
  constructor(p: number | string) {}
}
EOF
# This file shows the pattern - overloads come first without body, implementation last with body
cat /tmp/test_constructor.ts

Repository: mohanagy/graphify-ts

Length of output: 166


🏁 Script executed:

# Check how to properly handle this in ES2022 - look for similar patterns in codebase
grep -n "\.filter\|\.reverse\|for.*backward\|\.at\(" src/pipeline/spi/framework-nestjs.ts | head -20

Repository: mohanagy/graphify-ts

Length of output: 88


Select the implementation constructor before reading injected params.

find() returns the first constructor declaration, which may be an overload signature when the class uses constructor overloads. Parameter decorators and parameters exist only on the implementation constructor (the last one with a body), so selecting an overload signature causes emitted injects edges to be wrong or missing.

The suggested fix uses Array.prototype.findLast(), which is not available in the project's ES2022 target. Use an ES2022-compatible alternative instead:

Suggested fix
-  const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
+  const ctor =
+    [...classDecl.members]
+      .reverse()
+      .find(
+        (m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && m.body !== undefined,
+      ) ??
+    classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
   if (!ctor) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ctor = classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
if (!ctor) return
const ctor =
[...classDecl.members]
.reverse()
.find(
(m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m) && m.body !== undefined,
) ??
classDecl.members.find((m): m is ts.ConstructorDeclaration => ts.isConstructorDeclaration(m))
if (!ctor) return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/spi/framework-nestjs.ts` around lines 537 - 538, The current
code uses classDecl.members.find(...) to grab a constructor which may pick an
overload signature instead of the implementation; change the selection of ctor
(the constructor declaration) to explicitly choose the implementation
constructor (the one with a body) rather than the first match—e.g., filter
members by ts.isConstructorDeclaration and then pick the one with a .body (or
iterate members in reverse to find the first constructor with a body) before
reading injected params so injected edges are derived from the real
implementation constructor.

@mohanagy mohanagy merged commit f5c1387 into main May 10, 2026
7 checks passed
mohanagy added a commit that referenced this pull request May 11, 2026
…ata (#120)

Releases the v0.14 SPI substrate (issue #72) and all framework metadata work shipped in PRs #98 #99 #100..#119. See CHANGELOG.md for the full release notes.

Highlights:

- Complete SPI v1 substrate with TypeScript type-checker-backed file/symbol/call/type/test layers.

- Five framework detectors (NestJS, Express, Next.js, React Router, Redux Toolkit) with route_path metadata, dynamic-segment normalization, cross-file mount-prefix resolution, and structural factory metadata.

- SPI → ExtractionData projector that propagates framework_role + framework_metadata onto every ExtractionNode.

- Express trailing-slash normalization closes the most visible legacy/SPI divergence.

- Full byte-equivalence with the legacy extract() on demo-repo remains deferred (documented taxonomy divergence).
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