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:
parent
414a1433ba
commit
d8443b89e5
2 changed files with 83 additions and 9 deletions
|
|
@ -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, ".");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue