From 414a1433baf4ed4a61a158bdd3d6fb5c0aa13d5f Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sun, 22 Mar 2026 08:36:40 -0400 Subject: [PATCH] fix(skills): address QA round 17 QA17-1: Support multiline arrays inside [project.optional-dependencies] so FastAPI declared in multiline optional dependency groups is detected correctly. QA17-2: Extend Spring Boot version-catalog detection to bundle aliases (libs.bundles.*) by resolving bundles back to library aliases that map to org.springframework.boot artifacts. Add regression tests for: - multiline optional FastAPI dependencies - Spring Boot bundle alias detection --- src/resources/extensions/gsd/detection.ts | 30 +++++++++++++++-- .../extensions/gsd/tests/detection.test.ts | 32 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/detection.ts b/src/resources/extensions/gsd/detection.ts index e53df4461..00fd1ceb5 100644 --- a/src/resources/extensions/gsd/detection.ts +++ b/src/resources/extensions/gsd/detection.ts @@ -825,6 +825,7 @@ function containsSpringBootMarker( const springBootAliases = new Set(); const springBootLibraries = new Set(); + const springBootBundles = new Set(); for (const relativePath of versionCatalogFiles) { try { const raw = readBounded(join(basePath, relativePath), 64 * 1024); @@ -839,6 +840,18 @@ function containsSpringBootMarker( while ((match = libraryRe.exec(content)) !== null) { springBootLibraries.add(normalizePluginAlias(match[1])); } + + const bundleRe = /^\s*([A-Za-z0-9_.-]+)\s*=\s*\[([\s\S]*?)\]/gm; + while ((match = bundleRe.exec(content)) !== null) { + const bundleAlias = normalizePluginAlias(`bundles.${match[1]}`); + const referencedAliases = match[2] + .split(",") + .map((part) => normalizePluginAlias(part.replace(/["'\s]/g, ""))) + .filter(Boolean); + if (referencedAliases.some((alias) => springBootLibraries.has(alias))) { + springBootBundles.add(bundleAlias); + } + } } catch { // unreadable version catalog — continue scanning others } @@ -848,7 +861,7 @@ function containsSpringBootMarker( if (springBootAliases.has(alias)) return true; } for (const alias of usedLibraryAliases) { - if (springBootLibraries.has(alias)) return true; + if (springBootLibraries.has(alias) || springBootBundles.has(alias)) return true; } return false; @@ -888,6 +901,7 @@ function extractPyprojectDependencySections(content: string): string { const collected: string[] = []; let section = ""; let collectingProjectDeps = false; + let collectingOptionalDeps = false; let bracketDepth = 0; for (const line of lines) { @@ -902,6 +916,15 @@ function extractPyprojectDependencySections(content: string): string { continue; } + if (collectingOptionalDeps) { + collected.push(line); + bracketDepth += countChar(line, "[") - countChar(line, "]"); + if (bracketDepth <= 0) { + collectingOptionalDeps = false; + } + continue; + } + const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/); if (sectionMatch) { section = sectionMatch[1].trim(); @@ -923,7 +946,10 @@ function extractPyprojectDependencySections(content: string): string { if (section === "project.optional-dependencies") { const equalsIndex = line.indexOf("="); if (equalsIndex !== -1) { - collected.push(line.slice(equalsIndex + 1)); + const value = line.slice(equalsIndex + 1); + collected.push(value); + bracketDepth = countChar(value, "[") - countChar(value, "]"); + collectingOptionalDeps = bracketDepth > 0; } } 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 2dc5bd0a7..7435bf514 100644 --- a/src/resources/extensions/gsd/tests/detection.test.ts +++ b/src/resources/extensions/gsd/tests/detection.test.ts @@ -823,6 +823,21 @@ test("detectProjectSignals: pyproject optional-dependency group name does not tr } }); +test("detectProjectSignals: pyproject multiline optional dependency emits dep:fastapi", () => { + const dir = makeTempDir("signals-fastapi-pyproject-optional-multiline"); + try { + writeFileSync( + join(dir, "pyproject.toml"), + '[project]\ndependencies = ["flask>=3.0"]\n\n[project.optional-dependencies]\napi = [\n "fastapi>=0.115",\n "uvicorn>=0.30",\n]\n', + "utf-8", + ); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:fastapi"), "multiline optional dependency arrays should trigger FastAPI detection"); + } finally { + cleanup(dir); + } +}); + test("detectProjectSignals: FastAPI direct reference with @ emits dep:fastapi", () => { const dir = makeTempDir("signals-fastapi-direct-reference"); try { @@ -1060,3 +1075,20 @@ test("detectProjectSignals: Spring Boot version-catalog library alias emits dep: cleanup(dir); } }); + +test("detectProjectSignals: Spring Boot version-catalog bundle alias emits dep:spring-boot", () => { + const dir = makeTempDir("signals-spring-version-catalog-bundle"); + try { + mkdirSync(join(dir, "gradle"), { recursive: true }); + writeFileSync(join(dir, "build.gradle.kts"), "dependencies { implementation(libs.bundles.backend.web) }", "utf-8"); + writeFileSync( + join(dir, "gradle", "libs.versions.toml"), + "[libraries]\nspring-boot-starter-web = { module = 'org.springframework.boot:spring-boot-starter-web', version = '3.2.0' }\n\n[bundles]\nbackend-web = ['spring-boot-starter-web']\n", + "utf-8", + ); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "Spring Boot bundle aliases should trigger detection"); + } finally { + cleanup(dir); + } +});