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:
parent
c77d35e9f7
commit
aab9b0cb33
2 changed files with 63 additions and 11 deletions
|
|
@ -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 }];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue