Skip to content

Commit bfe2c42

Browse files
fix: add NestJS discriminator guards and fix EXPOSES edge target
- NestJSControllerDetector: bail out early if no @nestjs/ import to prevent false positives on Angular controllers and generic TypeScript - NestJSGuardsDetector: bail out early if no @nestjs/ import to prevent false positives on any TypeScript with canActivate() - NestJSControllerDetector: add edge.setTarget(node) on EXPOSES edges — previously missing, causing all class→endpoint edges to be silently dropped by GraphBuilder Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent c5f9852 commit bfe2c42

4 files changed

Lines changed: 69 additions & 7 deletions

File tree

src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetector.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class NestJSControllerDetector extends AbstractAntlrDetector {
3838
private static final Pattern FETCH_RE = Pattern.compile(
3939
"\\bfetch\\s*\\(\\s*['\"`]");
4040

41+
private static final Pattern NESTJS_IMPORT = Pattern.compile("from\\s+['\"]@nestjs/");
42+
4143
private static final Pattern CONTROLLER_PATTERN = Pattern.compile(
4244
"@Controller\\(\\s*['\"`]?([^'\"`\\)\\s]*)['\"`]?\\s*\\)(?:\\s*@\\w+\\([^)]*\\))*\\s*\\n\\s*(?:export\\s+)?class\\s+(\\w+)"
4345
);
@@ -78,9 +80,11 @@ public DetectorResult detect(DetectorContext ctx) {
7880

7981
@Override
8082
protected DetectorResult detectWithRegex(DetectorContext ctx) {
83+
String text = ctx.content();
84+
if (!NESTJS_IMPORT.matcher(text).find()) return DetectorResult.empty();
85+
8186
List<CodeNode> nodes = new ArrayList<>();
8287
List<CodeEdge> edges = new ArrayList<>();
83-
String text = ctx.content();
8488
String filePath = ctx.filePath();
8589
String moduleName = ctx.moduleName();
8690

@@ -161,6 +165,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) {
161165
edge.setId(classId + "->exposes->" + nodeId);
162166
edge.setKind(EdgeKind.EXPOSES);
163167
edge.setSourceId(classId);
168+
edge.setTarget(node);
164169
edges.add(edge);
165170
}
166171
}

src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetector.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
@Component
2929
public class NestJSGuardsDetector extends AbstractAntlrDetector {
3030

31+
private static final Pattern NESTJS_IMPORT = Pattern.compile("from\\s+['\"]@nestjs/");
32+
3133
private static final Pattern USE_GUARDS_PATTERN = Pattern.compile(
3234
"@UseGuards\\(\\s*([^)]+)\\)"
3335
);
@@ -67,8 +69,10 @@ public DetectorResult detect(DetectorContext ctx) {
6769

6870
@Override
6971
protected DetectorResult detectWithRegex(DetectorContext ctx) {
70-
List<CodeNode> nodes = new ArrayList<>();
7172
String text = ctx.content();
73+
if (!NESTJS_IMPORT.matcher(text).find()) return DetectorResult.empty();
74+
75+
List<CodeNode> nodes = new ArrayList<>();
7276
String filePath = ctx.filePath();
7377
String moduleName = ctx.moduleName();
7478

src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetectorTest.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.github.randomcodespace.iq.detector.DetectorContext;
44
import io.github.randomcodespace.iq.detector.DetectorResult;
55
import io.github.randomcodespace.iq.detector.DetectorTestUtils;
6+
import io.github.randomcodespace.iq.model.EdgeKind;
67
import io.github.randomcodespace.iq.model.NodeKind;
78
import org.junit.jupiter.api.Test;
89

@@ -15,6 +16,7 @@ class NestJSControllerDetectorTest {
1516
@Test
1617
void detectsNestJSController() {
1718
String code = """
19+
import { Controller, Get, Post } from '@nestjs/common';
1820
@Controller('users')
1921
export class UsersController {
2022
@Get()
@@ -35,8 +37,42 @@ export class UsersController {
3537
// Endpoints
3638
assertTrue(result.nodes().stream().anyMatch(n ->
3739
n.getKind() == NodeKind.ENDPOINT && "GET /users".equals(n.getLabel())));
38-
// EXPOSES edges
40+
// EXPOSES edges with valid targets
3941
assertEquals(3, result.edges().size());
42+
assertTrue(result.edges().stream().allMatch(e ->
43+
e.getKind() == EdgeKind.EXPOSES && e.getTarget() != null));
44+
}
45+
46+
@Test
47+
void noMatchWithoutNestJSImport() {
48+
// Generic TypeScript with @Controller-like patterns but no @nestjs import
49+
String code = """
50+
@Controller('users')
51+
export class UsersController {
52+
@Get()
53+
findAll() {}
54+
}
55+
""";
56+
DetectorContext ctx = DetectorTestUtils.contextFor("src/users.controller.ts", "typescript", code);
57+
DetectorResult result = detector.detect(ctx);
58+
assertTrue(result.nodes().isEmpty());
59+
assertTrue(result.edges().isEmpty());
60+
}
61+
62+
@Test
63+
void noMatchOnAngularComponent() {
64+
// Angular also uses @Component decorator, should not match NestJS
65+
String code = """
66+
import { Component } from '@angular/core';
67+
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
68+
export class AppComponent {
69+
@Get('/users')
70+
getUsers() {}
71+
}
72+
""";
73+
DetectorContext ctx = DetectorTestUtils.contextFor("src/app.component.ts", "typescript", code);
74+
DetectorResult result = detector.detect(ctx);
75+
assertTrue(result.nodes().isEmpty());
4076
}
4177

4278
@Test
@@ -49,8 +85,8 @@ void noMatchOnNonNestJSCode() {
4985

5086
@Test
5187
void deterministic() {
52-
String code = "@Controller('test')\nexport class TestController {\n @Get()\n find() {}\n}";
53-
DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code);
88+
String code = "import { Controller, Get } from '@nestjs/common';\n@Controller('test')\nexport class TestController {\n @Get()\n find() {}\n}";
89+
DetectorContext ctx = DetectorTestUtils.contextFor("src/test.controller.ts", "typescript", code);
5490
DetectorTestUtils.assertDeterministic(detector, ctx);
5591
}
5692
}

src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetectorTest.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class NestJSGuardsDetectorTest {
1515
@Test
1616
void detectsGuardsAndRoles() {
1717
String code = """
18+
import { Injectable, CanActivate } from '@nestjs/common';
19+
import { AuthGuard } from '@nestjs/passport';
1820
@UseGuards(JwtAuthGuard, RolesGuard)
1921
@Roles('admin', 'user')
2022
canActivate(context) {
@@ -34,6 +36,21 @@ void detectsGuardsAndRoles() {
3436
n.getLabel().contains("Roles(admin, user)")));
3537
}
3638

39+
@Test
40+
void noMatchWithoutNestJSImport() {
41+
// Generic TypeScript with canActivate() but no @nestjs import
42+
String code = """
43+
class RouteGuard {
44+
canActivate(context) {
45+
return this.authService.isAuthenticated();
46+
}
47+
}
48+
""";
49+
DetectorContext ctx = DetectorTestUtils.contextFor("src/route.guard.ts", "typescript", code);
50+
DetectorResult result = detector.detect(ctx);
51+
assertTrue(result.nodes().isEmpty());
52+
}
53+
3754
@Test
3855
void noMatchOnNonGuardCode() {
3956
String code = "class SomeService {}";
@@ -44,8 +61,8 @@ void noMatchOnNonGuardCode() {
4461

4562
@Test
4663
void deterministic() {
47-
String code = "@UseGuards(AuthGuard)\n@Roles('admin')";
48-
DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code);
64+
String code = "import { Injectable } from '@nestjs/common';\n@UseGuards(AuthGuard)\n@Roles('admin')";
65+
DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.guard.ts", "typescript", code);
4966
DetectorTestUtils.assertDeterministic(detector, ctx);
5067
}
5168
}

0 commit comments

Comments
 (0)