fix(skills): address QA round 18

QA18-1: Replace token-level FastAPI matching with package-name-aware
parsing for requirements.txt and pyproject dependency sections so extras
like my-sdk[fastapi] and unrelated fastapi tokens do not emit
dep:fastapi.

QA18-2: Scope direct Spring Boot detection to actual plugin/dependency
declarations instead of arbitrary spring-boot text, and fix Kotlin DSL
plugin syntax matching (id("org.springframework.boot")).

Add regression tests for:
- dependency extras mentioning fastapi
- build metadata mentioning spring-boot
- Kotlin DSL Spring Boot plugin detection
This commit is contained in:
Derek Pearson 2026-03-22 08:41:42 -04:00
parent 414a1433ba
commit d8443b89e5
2 changed files with 83 additions and 9 deletions

View file

@ -430,7 +430,7 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
const dependencyFiles = scannedFiles.filter((file) =>
file.endsWith("requirements.txt") || file.endsWith("pyproject.toml"),
);
if (containsDependencyMarker(basePath, dependencyFiles, "fastapi")) {
if (containsFastapiDependency(basePath, dependencyFiles)) {
pushUnique(detectedFiles, "dep:fastapi");
}
@ -769,13 +769,20 @@ function matchesProjectFileMarker(scannedFile: string, marker: string): boolean
);
}
function containsDependencyMarker(basePath: string, relativePaths: string[], marker: "fastapi"): boolean {
function containsFastapiDependency(basePath: string, relativePaths: string[]): boolean {
for (const relativePath of relativePaths) {
try {
const raw = readBounded(join(basePath, relativePath), 64 * 1024);
const content = extractDependencyContent(relativePath, raw).toLowerCase();
if (marker === "fastapi" && /\bfastapi(?=$|[\s<=>!~@\[\],;"'])/.test(content)) {
return true;
const content = extractDependencyContent(relativePath, raw);
if (relativePath.endsWith("requirements.txt")) {
for (const line of content.split("\n")) {
if (extractRequirementName(line) === "fastapi") return true;
}
continue;
}
if (relativePath.endsWith("pyproject.toml")) {
if (containsFastapiInPyproject(content)) return true;
}
} catch {
// unreadable file — continue scanning other candidate files
@ -796,19 +803,20 @@ function containsSpringBootMarker(
for (const relativePath of buildFiles) {
try {
const raw = readBounded(join(basePath, relativePath), 64 * 1024);
const content = stripDependencyComments(relativePath, raw).toLowerCase();
if (/(org\.springframework\.boot|spring-boot(?:-starter)?)/.test(content)) {
const content = stripDependencyComments(relativePath, raw);
if (containsDirectSpringBootReference(relativePath, content)) {
return true;
}
const normalized = content.toLowerCase();
const aliasRe = /alias\(\s*libs\.plugins\.([a-z0-9_.-]+)\s*\)/gi;
let match: RegExpExecArray | null;
while ((match = aliasRe.exec(content)) !== null) {
while ((match = aliasRe.exec(normalized)) !== null) {
usedPluginAliases.add(normalizePluginAlias(match[1]));
}
const libraryAliasRe = /\blibs\.((?!plugins\b)[a-z0-9_.-]+)/gi;
while ((match = libraryAliasRe.exec(content)) !== null) {
while ((match = libraryAliasRe.exec(normalized)) !== null) {
usedLibraryAliases.add(normalizePluginAlias(match[1]));
}
} catch {
@ -896,6 +904,46 @@ function extractDependencyContent(relativePath: string, content: string): string
return stripped;
}
function extractRequirementName(spec: string): string | null {
const trimmed = spec.trim().replace(/^["']|["']$/g, "");
if (!trimmed) return null;
const match = trimmed.match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?(?=\s*(?:@|[<>=!~;]|$))/);
if (!match) return null;
return normalizePackageName(match[1]);
}
function containsFastapiInPyproject(content: string): boolean {
for (const line of content.split("\n")) {
const keyMatch = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
if (keyMatch && normalizePackageName(keyMatch[1]) === "fastapi") {
return true;
}
}
const quotedSpecRe = /["']([^"']+)["']/g;
let match: RegExpExecArray | null;
while ((match = quotedSpecRe.exec(content)) !== null) {
if (extractRequirementName(match[1]) === "fastapi") {
return true;
}
}
return false;
}
function containsDirectSpringBootReference(relativePath: string, content: string): boolean {
if (relativePath.endsWith("pom.xml")) {
return /<groupId>\s*org\.springframework\.boot\s*<\/groupId>|<artifactId>\s*spring-boot[^<]*<\/artifactId>/i.test(content);
}
if (relativePath.endsWith("build.gradle") || relativePath.endsWith("build.gradle.kts")) {
return /(id\s*\(?\s*["']org\.springframework\.boot["']|(?:implementation|api|compileOnly|runtimeOnly|testImplementation|annotationProcessor|kapt)\s*\(?\s*["'][^"']*org\.springframework\.boot:[^"']*spring-boot[^"']*["'])/i.test(content);
}
return false;
}
function extractPyprojectDependencySections(content: string): string {
const lines = content.split("\n");
const collected: string[] = [];
@ -964,6 +1012,10 @@ function countChar(text: string, char: string): number {
return [...text].filter((c) => c === char).length;
}
function normalizePackageName(name: string): string {
return name.toLowerCase().replace(/[_.]/g, "-");
}
function normalizePluginAlias(alias: string): string {
return alias.toLowerCase().replace(/[-_]/g, ".");
}

View file

@ -882,6 +882,17 @@ test("detectProjectSignals: fastapi-* packages do not trigger dep:fastapi withou
}
});
test("detectProjectSignals: dependency extras mentioning fastapi do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-extra-only");
try {
writeFileSync(join(dir, "requirements.txt"), "my-sdk[fastapi]>=1.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "dependency extras should not imply FastAPI framework usage");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Django project does NOT get dep:fastapi marker", () => {
const dir = makeTempDir("signals-django-no-fastapi");
try {
@ -991,6 +1002,17 @@ test("detectProjectSignals: Android inline comments do not emit dep:spring-boot"
}
});
test("detectProjectSignals: build metadata mentioning spring-boot does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-metadata-only");
try {
writeFileSync(join(dir, "build.gradle"), 'def notes = "spring-boot migration planned later"', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "arbitrary metadata text should not trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot version-catalog alias emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog");
try {