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 <cursoragent@cursor.com>
This commit is contained in:
Gergo Magyar 2026-09-02 19:16:38 +00:00
parent 1f29c8ea97
commit f4f67f08ec
4 changed files with 27 additions and 15 deletions

View file

@ -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:

View file

@ -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;

View file

@ -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<string, string>): RepoConstants {
return new Map(
Object.entries(files).map(([filePath, source]) => [
@ -38,7 +46,6 @@ function constantsOf(files: Record<string, string>): RepoConstants {
);
}
describeKotlin('extractKotlinSpringRoutes', () => {
it('extracts direct RestController functions with independent class prefixes and handlers', () => {
const routes = extractKotlinSpringRoutes(
parse(`

View file

@ -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();