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.
This commit is contained in:
parent
da6f246891
commit
30d799e1b9
2 changed files with 89 additions and 4 deletions
|
|
@ -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<string>();
|
||||
const usedLibraryAliases = new Set<string>();
|
||||
|
||||
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<string>();
|
||||
const springBootLibraries = new Set<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue