fix(skills): address QA round 15
QA15-1: Extend exact FastAPI dependency detection to support common PEP 508 specifiers like ~= and !=. QA15-2: Scope pyproject.toml FastAPI detection to actual dependency sections ([project] dependencies array, [project.optional-dependencies], [tool.poetry.dependencies], and [tool.poetry.group.*.dependencies]) so metadata-only mentions do not emit dep:fastapi. QA15-3: Remove spring.boot alias-name matching from direct Spring Boot detection. Direct matches now require org.springframework.boot or spring-boot(-starter) strings; alias-based detection only succeeds via alias usage + version-catalog id correlation. QA15-4: When a nested Gradle Spring Boot service emits dep:spring-boot and no language was otherwise detected, set primaryLanguage to java/kotlin so onboarding no longer reports 'unknown project'. Add regression tests for: - FastAPI ~= operator - pyproject metadata-only fastapi mention - spring-like alias names without Spring Boot id - nested Spring Boot language hint
This commit is contained in:
parent
803913b13d
commit
da6f246891
2 changed files with 106 additions and 3 deletions
|
|
@ -440,6 +440,9 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
|
|||
const springBootVersionCatalogs = scannedFiles.filter((file) => file.endsWith("libs.versions.toml"));
|
||||
if (containsSpringBootMarker(basePath, springBootBuildFiles, springBootVersionCatalogs)) {
|
||||
pushUnique(detectedFiles, "dep:spring-boot");
|
||||
if (!primaryLanguage) {
|
||||
primaryLanguage = springBootBuildFiles.some((file) => file.endsWith("build.gradle.kts")) ? "kotlin" : "java/kotlin";
|
||||
}
|
||||
}
|
||||
|
||||
// Git repo detection
|
||||
|
|
@ -770,8 +773,8 @@ function containsDependencyMarker(basePath: string, relativePaths: string[], mar
|
|||
for (const relativePath of relativePaths) {
|
||||
try {
|
||||
const raw = readBounded(join(basePath, relativePath), 64 * 1024);
|
||||
const content = stripDependencyComments(relativePath, raw).toLowerCase();
|
||||
if (marker === "fastapi" && /\bfastapi(?=$|[\s<=>\[\],;"'])/.test(content)) {
|
||||
const content = extractDependencyContent(relativePath, raw).toLowerCase();
|
||||
if (marker === "fastapi" && /\bfastapi(?=$|[\s<=>!~\[\],;"'])/.test(content)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -793,7 +796,7 @@ function containsSpringBootMarker(
|
|||
try {
|
||||
const raw = readBounded(join(basePath, relativePath), 64 * 1024);
|
||||
const content = stripDependencyComments(relativePath, raw).toLowerCase();
|
||||
if (/(org\.springframework\.boot|spring[-.]boot(?:[-.]starter)?)/.test(content)) {
|
||||
if (/(org\.springframework\.boot|spring-boot(?:-starter)?)/.test(content)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -854,6 +857,62 @@ function stripDependencyComments(relativePath: string, content: string): string
|
|||
return content;
|
||||
}
|
||||
|
||||
function extractDependencyContent(relativePath: string, content: string): string {
|
||||
const stripped = stripDependencyComments(relativePath, content);
|
||||
if (relativePath.endsWith("pyproject.toml")) {
|
||||
return extractPyprojectDependencySections(stripped);
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
function extractPyprojectDependencySections(content: string): string {
|
||||
const lines = content.split("\n");
|
||||
const collected: string[] = [];
|
||||
let section = "";
|
||||
let collectingProjectDeps = false;
|
||||
let bracketDepth = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (collectingProjectDeps) {
|
||||
collected.push(line);
|
||||
bracketDepth += countChar(line, "[") - countChar(line, "]");
|
||||
if (bracketDepth <= 0) {
|
||||
collectingProjectDeps = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
||||
if (sectionMatch) {
|
||||
section = sectionMatch[1].trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section === "project" && /^dependencies\s*=\s*\[/.test(trimmed)) {
|
||||
collected.push(line);
|
||||
bracketDepth = countChar(line, "[") - countChar(line, "]");
|
||||
collectingProjectDeps = bracketDepth > 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
section === "project.optional-dependencies" ||
|
||||
section === "tool.poetry.dependencies" ||
|
||||
/^tool\.poetry\.group\.[^.]+\.dependencies$/.test(section)
|
||||
) {
|
||||
collected.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return collected.join("\n");
|
||||
}
|
||||
|
||||
function countChar(text: string, char: string): number {
|
||||
return [...text].filter((c) => c === char).length;
|
||||
}
|
||||
|
||||
function normalizePluginAlias(alias: string): string {
|
||||
return alias.toLowerCase().replace(/[-_]/g, ".");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -782,6 +782,32 @@ test("detectProjectSignals: FastAPI detected via pyproject.toml dependency", ()
|
|||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: FastAPI detected with PEP 508 ~= operator", () => {
|
||||
const dir = makeTempDir("signals-fastapi-compatible-release");
|
||||
try {
|
||||
writeFileSync(join(dir, "requirements.txt"), "fastapi~=0.115\n", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "~= should count as a FastAPI dependency");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: pyproject metadata mention does not trigger dep:fastapi", () => {
|
||||
const dir = makeTempDir("signals-fastapi-pyproject-metadata");
|
||||
try {
|
||||
writeFileSync(
|
||||
join(dir, "pyproject.toml"),
|
||||
'[project]\nname = "example"\nkeywords = ["fastapi"]\ndependencies = ["flask>=3.0"]\n',
|
||||
"utf-8",
|
||||
);
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "metadata-only mentions should not trigger FastAPI detection");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: FastAPI comments do not trigger dep:fastapi", () => {
|
||||
const dir = makeTempDir("signals-fastapi-comment");
|
||||
try {
|
||||
|
|
@ -875,6 +901,7 @@ test("detectProjectSignals: nested Spring Boot Gradle service emits dep:spring-b
|
|||
);
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "should detect nested Spring Boot Gradle service");
|
||||
assert.equal(signals.primaryLanguage, "java/kotlin");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
|
|
@ -956,3 +983,20 @@ test("detectProjectSignals: unused Spring Boot alias in libs.versions.toml does
|
|||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: spring-like alias name without Spring Boot id does not emit dep:spring-boot", () => {
|
||||
const dir = makeTempDir("signals-spring-version-catalog-false-alias");
|
||||
try {
|
||||
mkdirSync(join(dir, "gradle"), { recursive: true });
|
||||
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(libs.plugins.spring.boot.conventions) }", "utf-8");
|
||||
writeFileSync(
|
||||
join(dir, "gradle", "libs.versions.toml"),
|
||||
"[plugins]\nspring-boot-conventions = { id = 'com.example.conventions', version = '1.0.0' }\n",
|
||||
"utf-8",
|
||||
);
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "spring-looking alias names should not imply Spring Boot without matching id");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue