From 30d799e1b9fea5be15f0a9fb87873b8dc7b4f311 Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sun, 22 Mar 2026 08:28:56 -0400 Subject: [PATCH] fix(skills): address QA round 16 QA16-1: In pyproject.toml, treat [project.optional-dependencies] keys as extra names rather than dependency names by scanning only the right-hand-side values. Prevents extras named 'fastapi' from emitting dep:fastapi. QA16-2: Support FastAPI direct-reference requirements using the @ operator (e.g. fastapi @ https://...). QA16-3: Extend Spring Boot version-catalog detection to library aliases (e.g. implementation(libs.backend.web) + module = org.springframework.boot:spring-boot-starter-web), while keeping alias correlation strict. QA16-4: Use a neutral 'java/kotlin' language hint for nested Gradle Spring Boot services, even when they use build.gradle.kts, to avoid mislabeling Java codebases as Kotlin. Add regression tests for optional-dependency extras, direct-reference FastAPI, Spring Boot library aliases, and nested Gradle language hints. --- src/resources/extensions/gsd/detection.ts | 33 ++++++++-- .../extensions/gsd/tests/detection.test.ts | 60 +++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/resources/extensions/gsd/detection.ts b/src/resources/extensions/gsd/detection.ts index 674590426..e53df4461 100644 --- a/src/resources/extensions/gsd/detection.ts +++ b/src/resources/extensions/gsd/detection.ts @@ -441,7 +441,7 @@ export function detectProjectSignals(basePath: string): ProjectSignals { if (containsSpringBootMarker(basePath, springBootBuildFiles, springBootVersionCatalogs)) { pushUnique(detectedFiles, "dep:spring-boot"); if (!primaryLanguage) { - primaryLanguage = springBootBuildFiles.some((file) => file.endsWith("build.gradle.kts")) ? "kotlin" : "java/kotlin"; + primaryLanguage = "java/kotlin"; } } @@ -774,7 +774,7 @@ function containsDependencyMarker(basePath: string, relativePaths: string[], mar try { const raw = readBounded(join(basePath, relativePath), 64 * 1024); const content = extractDependencyContent(relativePath, raw).toLowerCase(); - if (marker === "fastapi" && /\bfastapi(?=$|[\s<=>!~\[\],;"'])/.test(content)) { + if (marker === "fastapi" && /\bfastapi(?=$|[\s<=>!~@\[\],;"'])/.test(content)) { return true; } } catch { @@ -791,6 +791,7 @@ function containsSpringBootMarker( versionCatalogFiles: string[], ): boolean { const usedPluginAliases = new Set(); + const usedLibraryAliases = new Set(); for (const relativePath of buildFiles) { try { @@ -805,16 +806,25 @@ function containsSpringBootMarker( while ((match = aliasRe.exec(content)) !== null) { usedPluginAliases.add(normalizePluginAlias(match[1])); } + + const libraryAliasRe = /\blibs\.((?!plugins\b)[a-z0-9_.-]+)/gi; + while ((match = libraryAliasRe.exec(content)) !== null) { + usedLibraryAliases.add(normalizePluginAlias(match[1])); + } } catch { // unreadable build file — continue scanning others } } - if (usedPluginAliases.size === 0 || versionCatalogFiles.length === 0) { + if (usedPluginAliases.size === 0 && usedLibraryAliases.size === 0) { + return false; + } + if (versionCatalogFiles.length === 0) { return false; } const springBootAliases = new Set(); + const springBootLibraries = new Set(); for (const relativePath of versionCatalogFiles) { try { const raw = readBounded(join(basePath, relativePath), 64 * 1024); @@ -824,6 +834,11 @@ function containsSpringBootMarker( while ((match = aliasRe.exec(content)) !== null) { springBootAliases.add(normalizePluginAlias(match[1])); } + + const libraryRe = /^\s*([A-Za-z0-9_.-]+)\s*=\s*\{[^\n}]*\b(module\s*=\s*["']org\.springframework\.boot:[^"']+["']|group\s*=\s*["']org\.springframework\.boot["'][^\n}]*\bname\s*=\s*["']spring-boot[^"']*["'])[^\n}]*\}/gm; + while ((match = libraryRe.exec(content)) !== null) { + springBootLibraries.add(normalizePluginAlias(match[1])); + } } catch { // unreadable version catalog — continue scanning others } @@ -832,6 +847,9 @@ function containsSpringBootMarker( for (const alias of usedPluginAliases) { if (springBootAliases.has(alias)) return true; } + for (const alias of usedLibraryAliases) { + if (springBootLibraries.has(alias)) return true; + } return false; } @@ -902,7 +920,14 @@ function extractPyprojectDependencySections(content: string): string { section === "tool.poetry.dependencies" || /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(section) ) { - collected.push(line); + if (section === "project.optional-dependencies") { + const equalsIndex = line.indexOf("="); + if (equalsIndex !== -1) { + collected.push(line.slice(equalsIndex + 1)); + } + } else { + collected.push(line); + } } } diff --git a/src/resources/extensions/gsd/tests/detection.test.ts b/src/resources/extensions/gsd/tests/detection.test.ts index 589fdc5bd..2dc5bd0a7 100644 --- a/src/resources/extensions/gsd/tests/detection.test.ts +++ b/src/resources/extensions/gsd/tests/detection.test.ts @@ -808,6 +808,32 @@ test("detectProjectSignals: pyproject metadata mention does not trigger dep:fast } }); +test("detectProjectSignals: pyproject optional-dependency group name does not trigger dep:fastapi", () => { + const dir = makeTempDir("signals-fastapi-pyproject-extra-name"); + try { + writeFileSync( + join(dir, "pyproject.toml"), + '[project]\ndependencies = ["flask>=3.0"]\n\n[project.optional-dependencies]\nfastapi = ["orjson>=3"]\n', + "utf-8", + ); + const signals = detectProjectSignals(dir); + assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "optional-dependency extra names should not trigger FastAPI detection"); + } finally { + cleanup(dir); + } +}); + +test("detectProjectSignals: FastAPI direct reference with @ emits dep:fastapi", () => { + const dir = makeTempDir("signals-fastapi-direct-reference"); + try { + writeFileSync(join(dir, "requirements.txt"), "fastapi @ https://example.com/fastapi.whl\n", "utf-8"); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:fastapi"), "direct-reference dependencies should trigger FastAPI detection"); + } finally { + cleanup(dir); + } +}); + test("detectProjectSignals: FastAPI comments do not trigger dep:fastapi", () => { const dir = makeTempDir("signals-fastapi-comment"); try { @@ -907,6 +933,23 @@ test("detectProjectSignals: nested Spring Boot Gradle service emits dep:spring-b } }); +test("detectProjectSignals: nested Spring Boot Kotlin DSL service still uses neutral java/kotlin language hint", () => { + const dir = makeTempDir("signals-spring-gradle-kts-nested"); + try { + mkdirSync(join(dir, "services", "api"), { recursive: true }); + writeFileSync( + join(dir, "services", "api", "build.gradle.kts"), + "plugins { id(\"org.springframework.boot\") version \"3.2.0\" }", + "utf-8", + ); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:spring-boot")); + assert.equal(signals.primaryLanguage, "java/kotlin"); + } finally { + cleanup(dir); + } +}); + test("detectProjectSignals: Android Gradle project does not emit dep:spring-boot", () => { const dir = makeTempDir("signals-android-no-spring"); try { @@ -1000,3 +1043,20 @@ test("detectProjectSignals: spring-like alias name without Spring Boot id does n cleanup(dir); } }); + +test("detectProjectSignals: Spring Boot version-catalog library alias emits dep:spring-boot", () => { + const dir = makeTempDir("signals-spring-version-catalog-library"); + try { + mkdirSync(join(dir, "gradle"), { recursive: true }); + writeFileSync(join(dir, "build.gradle.kts"), "dependencies { implementation(libs.backend.web) }", "utf-8"); + writeFileSync( + join(dir, "gradle", "libs.versions.toml"), + "[libraries]\nbackend-web = { module = 'org.springframework.boot:spring-boot-starter-web', version = '3.2.0' }\n", + "utf-8", + ); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "Spring Boot library aliases should trigger detection"); + } finally { + cleanup(dir); + } +});