fix(skills): address QA round 21

QA21-1: Recognize pip-tools style requirement manifests, including
requirements.in, requirements-dev.in, and files under requirements/*.in
or requirements/*.txt, for Python/FastAPI detection and nested marker
normalization.

QA21-2: Generalize Spring Boot version-catalog detection beyond the
default libs accessor by supporting any *.versions.toml catalog name and
matching its corresponding accessor in build.gradle(.kts).

Also fix the root-level requirements/base.in path matcher and add
regression tests for custom catalog accessors and pip-tools layouts.
This commit is contained in:
Derek Pearson 2026-03-22 08:56:20 -04:00
parent c77d35e9f7
commit aab9b0cb33
2 changed files with 63 additions and 11 deletions

View file

@ -440,7 +440,7 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
const springBootBuildFiles = scannedFiles.filter((file) =>
file.endsWith("pom.xml") || file.endsWith("build.gradle") || file.endsWith("build.gradle.kts"),
);
const springBootVersionCatalogs = scannedFiles.filter((file) => file.endsWith("libs.versions.toml"));
const springBootVersionCatalogs = scannedFiles.filter((file) => file.endsWith(".versions.toml"));
if (containsSpringBootMarker(basePath, springBootBuildFiles, springBootVersionCatalogs)) {
pushUnique(detectedFiles, "dep:spring-boot");
if (!primaryLanguage) {
@ -777,8 +777,9 @@ function isPythonRequirementsFile(relativePath: string): boolean {
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
return (
basename === "requirements.txt" ||
/^requirements([-.].+)?\.txt$/i.test(basename) ||
/\/requirements\/[^/]+\.txt$/i.test(normalized)
basename === "requirements.in" ||
/^requirements([-.].+)?\.(txt|in)$/i.test(basename) ||
/(^|\/)requirements\/[^/]+\.(txt|in)$/i.test(normalized)
);
}
@ -812,6 +813,7 @@ function containsSpringBootMarker(
): boolean {
const usedPluginAliases = new Set<string>();
const usedLibraryAliases = new Set<string>();
const catalogAccessors = new Set(versionCatalogFiles.map(versionCatalogAccessorName).filter(Boolean));
for (const relativePath of buildFiles) {
try {
@ -822,15 +824,17 @@ function containsSpringBootMarker(
}
const normalized = content.toLowerCase();
const aliasRe = /alias\(\s*libs\.plugins\.([a-z0-9_.-]+)\s*\)/gi;
let match: RegExpExecArray | null;
while ((match = aliasRe.exec(normalized)) !== null) {
usedPluginAliases.add(normalizePluginAlias(match[1]));
}
for (const accessor of catalogAccessors) {
const aliasRe = new RegExp(`alias\\(\\s*${accessor}\\.plugins\\.([a-z0-9_.-]+)\\s*\\)`, "gi");
while ((match = aliasRe.exec(normalized)) !== null) {
usedPluginAliases.add(normalizePluginAlias(match[1]));
}
const libraryAliasRe = /\blibs\.((?!plugins\b)[a-z0-9_.-]+)/gi;
while ((match = libraryAliasRe.exec(normalized)) !== null) {
usedLibraryAliases.add(normalizePluginAlias(match[1]));
const libraryAliasRe = new RegExp(`\\b${accessor}\\.((?!plugins\\b)[a-z0-9_.-]+)`, "gi");
while ((match = libraryAliasRe.exec(normalized)) !== null) {
usedLibraryAliases.add(normalizePluginAlias(match[1]));
}
}
} catch {
// unreadable build file — continue scanning others
@ -901,7 +905,7 @@ function stripDependencyComments(relativePath: string, content: string): string
if (relativePath.endsWith("pyproject.toml")) {
return content.replace(/(^|\s)#.*$/gm, "");
}
if (relativePath.endsWith("libs.versions.toml")) {
if (relativePath.endsWith(".versions.toml")) {
return content.replace(/(^|\s)#.*$/gm, "");
}
if (relativePath.endsWith("pom.xml")) {
@ -1045,6 +1049,12 @@ function normalizePluginAlias(alias: string): string {
return alias.toLowerCase().replace(/[-_]/g, ".");
}
function versionCatalogAccessorName(relativePath: string): string {
const normalized = relativePath.replaceAll("\\", "/");
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
return basename.replace(/\.versions\.toml$/i, "").toLowerCase();
}
function scanProjectFiles(basePath: string): string[] {
const files: string[] = [];
const queue: Array<{ path: string; depth: number }> = [{ path: basePath, depth: 0 }];

View file

@ -864,6 +864,31 @@ test("detectProjectSignals: FastAPI direct reference with @ emits dep:fastapi",
}
});
test("detectProjectSignals: FastAPI detected via requirements.in", () => {
const dir = makeTempDir("signals-fastapi-requirements-in");
try {
writeFileSync(join(dir, "requirements.in"), "fastapi>=0.115\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "requirements.in should trigger FastAPI detection");
assert.ok(signals.detectedFiles.includes("requirements.txt"), "requirements.in should normalize to requirements.txt marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via nested requirements/base.in", () => {
const dir = makeTempDir("signals-fastapi-requirements-dir-in");
try {
mkdirSync(join(dir, "requirements"), { recursive: true });
writeFileSync(join(dir, "requirements", "base.in"), "fastapi>=0.115\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "requirements/base.in should trigger FastAPI detection");
assert.ok(signals.detectedFiles.includes("requirements.txt"), "requirements/base.in should normalize to requirements.txt marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI comments do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-comment");
try {
@ -1155,3 +1180,20 @@ test("detectProjectSignals: Spring Boot version-catalog bundle alias emits dep:s
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot custom version-catalog accessor emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-custom-accessor");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(backend.plugins.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "backend.versions.toml"),
"[plugins]\nweb = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "custom version-catalog accessors should trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});