From f4f67f08ecbd986bd6f29f00f0bb5295372a9dca Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 2 Sep 2026 19:16:38 +0000 Subject: [PATCH] fix(route): isolate Win alias tests and guard unnamed class mappings Stub the vendor-prefix env in alias tests and fail closed when a class mapping has no name. Co-authored-by: Cursor --- .../core/group/extractors/http-patterns/kotlin.ts | 10 +++++----- .../src/core/ingestion/route-extractors/spring.ts | 8 +++++--- .../test/unit/kotlin-spring-route-ingestion.test.ts | 11 +++++++++-- .../unit/spring-vendor-annotation-alias.test.ts | 13 ++++++++----- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/gitnexus/src/core/group/extractors/http-patterns/kotlin.ts b/gitnexus/src/core/group/extractors/http-patterns/kotlin.ts index 65533e7e3..b00a3e400 100644 --- a/gitnexus/src/core/group/extractors/http-patterns/kotlin.ts +++ b/gitnexus/src/core/group/extractors/http-patterns/kotlin.ts @@ -450,11 +450,6 @@ function inferKotlinOkHttpMethod(urlCall: Parser.SyntaxNode): string | null { return name === null ? 'GET' : name.toUpperCase(); } -/** - * Build the plugin only if the Kotlin grammar is available. Compiling - * the queries against a null grammar would throw at module load time - * and abort the whole http-route-extractor module. - */ function enclosingAnnotationText(node: Parser.SyntaxNode): string { for (let current: Parser.SyntaxNode | null = node; current; current = current.parent) { if (current.type === 'annotation') return current.text; @@ -462,6 +457,11 @@ function enclosingAnnotationText(node: Parser.SyntaxNode): string { return node.text; } +/** + * Build the plugin only if the Kotlin grammar is available. Compiling + * the queries against a null grammar would throw at module load time + * and abort the whole http-route-extractor module. + */ function buildKotlinPlugin(language: unknown): HttpLanguagePlugin { // ─── Provider: Spring class-level @RequestMapping prefix ────────────── // Two patterns mirror the Java plugin's positional vs named split: diff --git a/gitnexus/src/core/ingestion/route-extractors/spring.ts b/gitnexus/src/core/ingestion/route-extractors/spring.ts index d8118f36a..701e7c03f 100644 --- a/gitnexus/src/core/ingestion/route-extractors/spring.ts +++ b/gitnexus/src/core/ingestion/route-extractors/spring.ts @@ -442,11 +442,13 @@ function annotationHasRouteMember(ann: Parser.SyntaxNode): boolean { /** Static class/interface-level RequestMapping method constraint, or wildcard by default. */ function typeRequestMethods(typeNode: Parser.SyntaxNode): readonly string[] { const mappings = declarationAnnotations(typeNode).filter((ann) => - isClassLevelMappingAnnotation(annotationName(ann)), + isClassLevelMappingAnnotation(annotationName(ann) ?? ''), ); if (mappings.length === 0) return ['*']; if (mappings.length !== 1) return []; - return springAnnotationHttpMethods(annotationName(mappings[0]), mappings[0].text); + const mappingName = annotationName(mappings[0]); + if (!mappingName) return []; + return springAnnotationHttpMethods(mappingName, mappings[0].text); } function annotationRoutePathsOrDefault(ann: Parser.SyntaxNode): string[] { @@ -459,7 +461,7 @@ function annotationRoutePathsOrDefault(ann: Parser.SyntaxNode): string[] { function typeClassPrefixes(typeNode: Parser.SyntaxNode): string[] { const prefixes: string[] = []; for (const ann of declarationAnnotations(typeNode)) { - if (isClassLevelMappingAnnotation(annotationName(ann))) + if (isClassLevelMappingAnnotation(annotationName(ann) ?? '')) prefixes.push(...annotationRoutePaths(ann)); } return prefixes; diff --git a/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts b/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts index 59379c7ae..8b1f8a6c7 100644 --- a/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts +++ b/gitnexus/test/unit/kotlin-spring-route-ingestion.test.ts @@ -4,7 +4,7 @@ * The Kotlin grammar is optional. Importing the extractor itself must not load * that grammar; only this guarded test setup does. */ -import { describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import Parser from 'tree-sitter'; import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js'; import { extractKotlinSpringRoutes } from '../../src/core/ingestion/route-extractors/kotlin-spring.js'; @@ -29,6 +29,14 @@ if (Kotlin) parser.setLanguage(Kotlin as Parser.Language); const parse = (source: string): Parser.Tree => parser.parse(source); const describeKotlin = Kotlin ? describe : describe.skip; +describeKotlin('extractKotlinSpringRoutes', () => { + beforeEach(() => { + vi.stubEnv('GITNEXUS_SPRING_VENDOR_PREFIXES', 'Win'); + }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + function constantsOf(files: Record): RepoConstants { return new Map( Object.entries(files).map(([filePath, source]) => [ @@ -38,7 +46,6 @@ function constantsOf(files: Record): RepoConstants { ); } -describeKotlin('extractKotlinSpringRoutes', () => { it('extracts direct RestController functions with independent class prefixes and handlers', () => { const routes = extractKotlinSpringRoutes( parse(` diff --git a/gitnexus/test/unit/spring-vendor-annotation-alias.test.ts b/gitnexus/test/unit/spring-vendor-annotation-alias.test.ts index a34f02c20..317b0beb6 100644 --- a/gitnexus/test/unit/spring-vendor-annotation-alias.test.ts +++ b/gitnexus/test/unit/spring-vendor-annotation-alias.test.ts @@ -12,7 +12,7 @@ * 3. End-to-end `extractSpringRoutes` with a fixture using vendor annotations * 4. Parity: both ingestion and group extractors surface the same routes */ -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import Parser from 'tree-sitter'; import Java from 'tree-sitter-java'; import { @@ -30,6 +30,13 @@ function parse(code: string): Parser.Tree { return parser.parse(code); } +beforeEach(() => { + vi.stubEnv('GITNEXUS_SPRING_VENDOR_PREFIXES', 'Win'); +}); +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe('resolveSpringAnnotationAlias', () => { it('returns undefined for exact built-in shortcut annotations', () => { expect(resolveSpringAnnotationAlias('PostMapping')).toBeUndefined(); @@ -79,10 +86,6 @@ describe('resolveSpringAnnotationAlias', () => { }); describe('Spring vendor prefix freshness', () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - it('canonicalizes equivalent lists regardless of order and duplicates', () => { vi.stubEnv('GITNEXUS_SPRING_VENDOR_PREFIXES', ' Win,Acme,Win '); const first = springVendorPrefixesKey();