@dailephd/my-dev-kit-lab 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -18
- package/dist/scripts/audits/runAudit.js +8 -2
- package/dist/scripts/security/validate.js +16 -47
- package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +191 -1
- package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +202 -4
- package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +42 -9
- package/dist/src/audits/codeRot/detectors/testRotDetector.js +241 -1
- package/dist/src/audits/codeRot/utils/filePatternUtils.js +19 -1
- package/dist/src/audits/codeRot/utils/jvmSourceFactsUtils.js +153 -0
- package/dist/src/audits/core/auditConfig.js +2 -2
- package/dist/src/audits/core/auditRunner.js +19 -0
- package/dist/src/audits/core/auditTypes.js +14 -7
- package/dist/src/audits/core/javaAnalyzer.js +282 -0
- package/dist/src/audits/core/jvmProjectMetadata.js +106 -0
- package/dist/src/audits/core/kotlinAnalyzer.js +280 -0
- package/dist/src/audits/core/languageAnalyzerRegistry.js +12 -3
- package/dist/src/audits/core/projectInventory.js +76 -3
- package/dist/src/audits/core/pythonAnalyzer.js +328 -0
- package/dist/src/audits/core/pythonProjectMetadata.js +80 -0
- package/dist/src/audits/report/auditReportModel.js +3 -0
- package/dist/src/audits/report/renderAuditJsonReport.js +2 -0
- package/dist/src/audits/report/renderAuditTextReport.js +40 -0
- package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +70 -0
- package/dist/src/audits/security/securityAuditAdapter.js +70 -0
- package/dist/src/audits/security/securityAuditTypes.js +17 -0
- package/dist/src/securityValidation/report/buildSecurityReport.js +31 -0
- package/dist/src/securityValidation/report/writeSecurityReportFiles.js +28 -0
- package/dist/src/securityValidation/validate/resolveTarget.js +9 -0
- package/package.json +6 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { collectJvmProjectMetadata } from "../../core/jvmProjectMetadata.js";
|
|
3
4
|
import { currentClaimPatternFor, plannedClaimPatternFor, FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN, isNegatedNearby, } from "../utils/docClaimPatterns.js";
|
|
4
5
|
import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
|
|
5
6
|
import { splitLines } from "../utils/textLines.js";
|
|
@@ -27,20 +28,95 @@ const FEATURE_CHECKS = [
|
|
|
27
28
|
featureId: "security-validation",
|
|
28
29
|
label: "security validation",
|
|
29
30
|
subjectPattern: "security validation",
|
|
30
|
-
isImplemented: (
|
|
31
|
+
isImplemented: (ctx) => ctx.sourceOfTruth.security.hasSecurityValidationSourceDir && ctx.sourceOfTruth.security.hasSecurityValidateScript,
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
featureId: "experiment-framework",
|
|
34
35
|
label: "experiment framework",
|
|
35
36
|
subjectPattern: "experiment (?:framework|plugin)",
|
|
36
|
-
isImplemented: (
|
|
37
|
+
isImplemented: (ctx) => ctx.sourceOfTruth.experiment.hasExperimentsSourceDir && ctx.sourceOfTruth.experiment.hasExperimentListScript,
|
|
38
|
+
},
|
|
39
|
+
// v0.3.3 Batch 3 -- Gradle/Maven/Android feature-claim checks, using only
|
|
40
|
+
// static JVM metadata (see jvmProjectMetadata.ts). Never runs Gradle,
|
|
41
|
+
// Maven, or any Android tooling.
|
|
42
|
+
{
|
|
43
|
+
featureId: "gradle-support",
|
|
44
|
+
label: "Gradle",
|
|
45
|
+
subjectPattern: "Gradle",
|
|
46
|
+
isImplemented: (ctx) => ctx.jvmMetadata.hasGradleBuild || ctx.jvmMetadata.hasGradleSettings,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
featureId: "maven-support",
|
|
50
|
+
label: "Maven",
|
|
51
|
+
subjectPattern: "Maven",
|
|
52
|
+
isImplemented: (ctx) => ctx.jvmMetadata.hasMavenPom,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
featureId: "android-validation",
|
|
56
|
+
label: "Android validation",
|
|
57
|
+
// Negative lookbehind excludes "(non-Android)"-style negated mentions
|
|
58
|
+
// (this project's own docs legitimately describe things as
|
|
59
|
+
// "the general (non-Android) ... adapter", which is a claim about the
|
|
60
|
+
// *adapter*, not about Android -- without this guard, "is implemented"
|
|
61
|
+
// appearing later in the same sentence about the adapter would
|
|
62
|
+
// falsely match as an Android claim).
|
|
63
|
+
subjectPattern: "(?<!non-)Android(?: validation| support)?",
|
|
64
|
+
// Android automated validation is not implemented anywhere in this
|
|
65
|
+
// project (see the v0.3.x/v0.4.x roadmap) -- this is a permanently
|
|
66
|
+
// false signal, not a metadata lookup, so a claim that it is
|
|
67
|
+
// implemented/current is always a genuine mismatch until a future
|
|
68
|
+
// version actually builds it. This does not implement any Android
|
|
69
|
+
// detection itself (no AndroidManifest.xml/Gradle-Android-plugin/
|
|
70
|
+
// Compose/AGP/SDK/emulator awareness) -- it only reuses the existing
|
|
71
|
+
// current-vs-planned claim mechanism for a feature this project
|
|
72
|
+
// already knows, out of band, does not exist.
|
|
73
|
+
isImplemented: () => false,
|
|
37
74
|
},
|
|
38
75
|
];
|
|
76
|
+
// v0.3.3 Batch 3 -- backtick-quoted-only Java/Kotlin FQCN-shaped symbol
|
|
77
|
+
// scan (T1-T4). Requiring the mention to be inside an inline code span
|
|
78
|
+
// (`` `com.example.Foo` ``) is what keeps this conservative: ordinary prose
|
|
79
|
+
// mentioning a capitalized word is extremely common and would be far too
|
|
80
|
+
// noisy to scan directly, but a backtick-quoted, fully-dotted,
|
|
81
|
+
// lowercase-package-then-Capitalized-name token is specifically how a
|
|
82
|
+
// real symbol reference is conventionally written in markdown docs.
|
|
83
|
+
const BACKTICK_SPAN_PATTERN = /`([^`]+)`/g;
|
|
84
|
+
const JVM_FQCN_PATTERN = /^[a-z][\w]*(?:\.[a-z][\w]*)*\.[A-Z]\w*$/;
|
|
85
|
+
// Reused by both the symbol-claim check and the Gradle/Maven command-claim
|
|
86
|
+
// check below: a broader set of future/negative-boundary hedge phrases than
|
|
87
|
+
// docClaimPatterns.ts's own HEDGE_PATTERN (which is narrowly scoped to
|
|
88
|
+
// "not yet published"-style wording) -- this project's own docs
|
|
89
|
+
// legitimately say things like "Android support is planned" or "does not
|
|
90
|
+
// support Gradle yet", which must never be treated as a current-state claim.
|
|
91
|
+
const JVM_FUTURE_OR_NEGATIVE_HEDGE_PATTERN = /\b(planned|future|deferred|not yet|out of scope|does not support|doesn't support|will support|not implemented|not supported)\b/i;
|
|
92
|
+
const GRADLE_COMMAND_PATTERN = /\b(?:\.\/)?gradlew(?:\.bat)?\b|\bgradle\b/i;
|
|
93
|
+
const MAVEN_COMMAND_PATTERN = /\bmvnw(?:\.cmd)?\b|\bmvn\b/i;
|
|
94
|
+
// Declaration kinds treated as safe, implementation-weight symbol claims --
|
|
95
|
+
// same conservative kind allowlist Batch 2's duplicate/dead-code checks
|
|
96
|
+
// already use (methods/variables/constants excluded as too noisy/ambiguous
|
|
97
|
+
// for a name-only, no-package-tracking cross-check).
|
|
98
|
+
const JVM_SYMBOL_DECLARATION_KINDS = new Set(["class", "interface", "enum", "function"]);
|
|
99
|
+
function collectKnownJvmDeclarationNames(ctx) {
|
|
100
|
+
const names = new Set();
|
|
101
|
+
if (!ctx.sourceFacts)
|
|
102
|
+
return names;
|
|
103
|
+
for (const file of ctx.sourceFacts.files) {
|
|
104
|
+
if (file.language !== "java" && file.language !== "kotlin")
|
|
105
|
+
continue;
|
|
106
|
+
if (file.parseStatus !== "parsed")
|
|
107
|
+
continue;
|
|
108
|
+
for (const decl of file.declarations) {
|
|
109
|
+
if (JVM_SYMBOL_DECLARATION_KINDS.has(decl.kind))
|
|
110
|
+
names.add(decl.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return names;
|
|
114
|
+
}
|
|
39
115
|
export const DOCS_CODE_MISMATCH_DETECTOR = {
|
|
40
116
|
id: DETECTOR_ID,
|
|
41
117
|
auditType: "code-rot",
|
|
42
118
|
title: "Documentation/code mismatch",
|
|
43
|
-
description: "Detects deterministic mismatches between documentation claims and source-of-truth data: feature-implementation claims
|
|
119
|
+
description: "Detects deterministic mismatches between documentation claims and source-of-truth data: feature-implementation claims, full-release-gate-for-scoped-checks wording, and (v0.3.3) Java/Kotlin symbol claims plus Gradle/Maven feature/command claims against static JVM metadata.",
|
|
44
120
|
supportedIncludeAreas: ["docs", "architecture", "cli", "package"],
|
|
45
121
|
shouldSkip: (ctx) => {
|
|
46
122
|
if (!ctx.config.include.includes("docs")) {
|
|
@@ -50,6 +126,13 @@ export const DOCS_CODE_MISMATCH_DETECTOR = {
|
|
|
50
126
|
},
|
|
51
127
|
run: (ctx) => {
|
|
52
128
|
const issues = [];
|
|
129
|
+
// v0.3.3 Batch 3 -- computed once per run, same "cheap, static,
|
|
130
|
+
// presence-only" treatment as pythonMetadata/jvmMetadata elsewhere in
|
|
131
|
+
// this codebase (see testRotDetector.ts). Never runs Gradle/Maven/any
|
|
132
|
+
// JVM tooling.
|
|
133
|
+
const jvmMetadata = collectJvmProjectMetadata(ctx.target.rootPath, ctx.inventory);
|
|
134
|
+
const featureCtx = { sourceOfTruth: ctx.sourceOfTruth, jvmMetadata };
|
|
135
|
+
const knownJvmDeclarationNames = collectKnownJvmDeclarationNames(ctx);
|
|
53
136
|
for (const docFile of ctx.inventory.docsFiles) {
|
|
54
137
|
const content = readBoundedText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes);
|
|
55
138
|
if (content === null)
|
|
@@ -58,9 +141,12 @@ export const DOCS_CODE_MISMATCH_DETECTOR = {
|
|
|
58
141
|
for (let i = 0; i < lines.length; i++) {
|
|
59
142
|
const line = lines[i];
|
|
60
143
|
for (const feature of FEATURE_CHECKS) {
|
|
144
|
+
if (feature.featureId === "security-validation" && isAndroidScopedSecurityValidationClaim(line)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
61
147
|
const currentMatch = currentClaimPatternFor(feature.subjectPattern).test(line);
|
|
62
148
|
const plannedMatch = plannedClaimPatternFor(feature.subjectPattern).test(line);
|
|
63
|
-
const implemented = feature.isImplemented(
|
|
149
|
+
const implemented = feature.isImplemented(featureCtx);
|
|
64
150
|
if (currentMatch && plannedMatch) {
|
|
65
151
|
// Same line claims both current and planned for the same
|
|
66
152
|
// subject -- genuinely ambiguous, reported as low-confidence
|
|
@@ -78,11 +164,120 @@ export const DOCS_CODE_MISMATCH_DETECTOR = {
|
|
|
78
164
|
if (FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN.test(line) && !isNegatedNearby(line)) {
|
|
79
165
|
issues.push(makeFullReleaseGateIssue(docFile.relativePath, i + 1, line));
|
|
80
166
|
}
|
|
167
|
+
if (!JVM_FUTURE_OR_NEGATIVE_HEDGE_PATTERN.test(line)) {
|
|
168
|
+
issues.push(...findJvmSymbolClaimMismatches(docFile.relativePath, i + 1, line, jvmMetadata, knownJvmDeclarationNames));
|
|
169
|
+
issues.push(...findJvmBuildCommandClaimMismatches(docFile.relativePath, i + 1, line, jvmMetadata));
|
|
170
|
+
}
|
|
81
171
|
}
|
|
82
172
|
}
|
|
83
173
|
return deduplicateIssuesById(issues);
|
|
84
174
|
},
|
|
85
175
|
};
|
|
176
|
+
// v0.3.3 Batch 3 -- T1/T2/T3/T4. Gated on the project having ANY real
|
|
177
|
+
// Java/Kotlin source at all (jvmMetadata.hasJavaSource/hasKotlinSource) --
|
|
178
|
+
// without this gate, a project with no JVM code whatsoever (e.g. this repo
|
|
179
|
+
// itself) would have every backtick-quoted FQCN-shaped mention trivially
|
|
180
|
+
// "fail" to match anything, which would be pure noise rather than a real
|
|
181
|
+
// signal. Matches by simple declaration name only (see
|
|
182
|
+
// collectKnownJvmDeclarationNames()'s header comment) -- this cannot verify
|
|
183
|
+
// the claimed package prefix actually matches, only that some declaration
|
|
184
|
+
// with that name exists somewhere in the scanned Java/Kotlin source, which
|
|
185
|
+
// is why this is always a "weak signal" candidate, never a proof.
|
|
186
|
+
function findJvmSymbolClaimMismatches(relativePath, lineNumber, line, jvmMetadata, knownJvmDeclarationNames) {
|
|
187
|
+
if (!jvmMetadata.hasJavaSource && !jvmMetadata.hasKotlinSource)
|
|
188
|
+
return [];
|
|
189
|
+
const issues = [];
|
|
190
|
+
BACKTICK_SPAN_PATTERN.lastIndex = 0;
|
|
191
|
+
for (const match of line.matchAll(BACKTICK_SPAN_PATTERN)) {
|
|
192
|
+
const candidate = match[1];
|
|
193
|
+
if (!JVM_FQCN_PATTERN.test(candidate))
|
|
194
|
+
continue;
|
|
195
|
+
const simpleName = candidate.slice(candidate.lastIndexOf(".") + 1);
|
|
196
|
+
if (knownJvmDeclarationNames.has(simpleName))
|
|
197
|
+
continue;
|
|
198
|
+
issues.push(makeCodeRotIssue({
|
|
199
|
+
auditType: "code-rot",
|
|
200
|
+
detectorId: DETECTOR_ID,
|
|
201
|
+
idCues: [relativePath, "jvm-symbol-claim-mismatch", String(lineNumber), candidate],
|
|
202
|
+
title: `Docs mention a Java/Kotlin symbol not found in static source facts: "${candidate}"`,
|
|
203
|
+
description: `${relativePath}:${lineNumber} mentions \`${candidate}\`, but no scanned Java/Kotlin declaration named "${simpleName}" was found. This is a static source-facts signal from a conservative, name-only scan -- it cannot verify the claimed package prefix, only that no declaration with this simple name exists anywhere in the scanned Java/Kotlin file set, so this is a candidate docs-code mismatch (weak signal), not proof.`,
|
|
204
|
+
severity: "info",
|
|
205
|
+
confidence: "low",
|
|
206
|
+
falsePositiveRisk: "high",
|
|
207
|
+
category: "docs-feature-mismatch",
|
|
208
|
+
recommendedAction: `Confirm "${candidate}" exists (it may be defined outside the scanned source set) before treating this as stale documentation.`,
|
|
209
|
+
suggestedFixStrategy: `Update ${relativePath} to reference an existing Java/Kotlin symbol, or add the missing declaration.`,
|
|
210
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
211
|
+
releaseBlocking: false,
|
|
212
|
+
implementationBlocking: false,
|
|
213
|
+
autoFixEligible: false,
|
|
214
|
+
evidence: [
|
|
215
|
+
{
|
|
216
|
+
kind: "file",
|
|
217
|
+
message: `Static source-facts check did not find a Java/Kotlin declaration named "${simpleName}" in the scanned file set.`,
|
|
218
|
+
filePath: relativePath,
|
|
219
|
+
line: lineNumber,
|
|
220
|
+
excerpt: line.trim().slice(0, 200),
|
|
221
|
+
source: DETECTOR_ID,
|
|
222
|
+
confidence: "low",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
affectedFiles: [relativePath],
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
return issues;
|
|
229
|
+
}
|
|
230
|
+
// v0.3.3 Batch 3 -- T6/T7/T8/T9. Only matches a Gradle/Maven command token
|
|
231
|
+
// inside a backtick-quoted inline code span, for the same "conventionally
|
|
232
|
+
// how a real command example is written" reasoning as the symbol check
|
|
233
|
+
// above. This never claims the command would actually succeed -- only that
|
|
234
|
+
// docs present a command example implying a build tool this project's
|
|
235
|
+
// static metadata does not show any trace of.
|
|
236
|
+
function findJvmBuildCommandClaimMismatches(relativePath, lineNumber, line, jvmMetadata) {
|
|
237
|
+
const issues = [];
|
|
238
|
+
BACKTICK_SPAN_PATTERN.lastIndex = 0;
|
|
239
|
+
for (const match of line.matchAll(BACKTICK_SPAN_PATTERN)) {
|
|
240
|
+
const candidate = match[1];
|
|
241
|
+
if (GRADLE_COMMAND_PATTERN.test(candidate) && !jvmMetadata.hasGradleBuild && !jvmMetadata.hasGradleSettings && !jvmMetadata.hasGradleWrapper) {
|
|
242
|
+
issues.push(makeBuildCommandIssue(relativePath, lineNumber, line, candidate, "Gradle", "no Gradle execution was performed"));
|
|
243
|
+
}
|
|
244
|
+
if (MAVEN_COMMAND_PATTERN.test(candidate) && !jvmMetadata.hasMavenPom) {
|
|
245
|
+
issues.push(makeBuildCommandIssue(relativePath, lineNumber, line, candidate, "Maven", "no Maven execution was performed"));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return issues;
|
|
249
|
+
}
|
|
250
|
+
function makeBuildCommandIssue(relativePath, lineNumber, line, commandExcerpt, toolLabel, executionNote) {
|
|
251
|
+
return makeCodeRotIssue({
|
|
252
|
+
auditType: "code-rot",
|
|
253
|
+
detectorId: DETECTOR_ID,
|
|
254
|
+
idCues: [relativePath, `${toolLabel.toLowerCase()}-command-claim-mismatch`, String(lineNumber), commandExcerpt],
|
|
255
|
+
title: `Docs include a ${toolLabel} command example, but no ${toolLabel} metadata was found: "${commandExcerpt}"`,
|
|
256
|
+
description: `${relativePath}:${lineNumber} shows the command \`${commandExcerpt}\`, but no ${toolLabel} build/config metadata was found in the project (see jvmProjectMetadata.ts's static presence checks). This is a candidate docs-code mismatch (weak signal), not proof -- ${executionNote}, so this scan cannot confirm the command would actually run.`,
|
|
257
|
+
severity: "info",
|
|
258
|
+
confidence: "low",
|
|
259
|
+
falsePositiveRisk: "high",
|
|
260
|
+
category: "docs-feature-mismatch",
|
|
261
|
+
recommendedAction: `Confirm ${toolLabel} metadata exists (it may live outside the scanned inventory) before treating this as stale documentation.`,
|
|
262
|
+
suggestedFixStrategy: `Update ${relativePath} to remove or correct the ${toolLabel} command example, or add the corresponding ${toolLabel} project files.`,
|
|
263
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
264
|
+
releaseBlocking: false,
|
|
265
|
+
implementationBlocking: false,
|
|
266
|
+
autoFixEligible: false,
|
|
267
|
+
evidence: [
|
|
268
|
+
{
|
|
269
|
+
kind: "file",
|
|
270
|
+
message: `Static source-facts/metadata check did not find ${toolLabel} build metadata; ${executionNote}.`,
|
|
271
|
+
filePath: relativePath,
|
|
272
|
+
line: lineNumber,
|
|
273
|
+
excerpt: line.trim().slice(0, 200),
|
|
274
|
+
source: DETECTOR_ID,
|
|
275
|
+
confidence: "low",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
affectedFiles: [relativePath],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
86
281
|
function makeCurrentButMissingIssue(relativePath, lineNumber, excerpt, feature) {
|
|
87
282
|
return makeCodeRotIssue({
|
|
88
283
|
auditType: "code-rot",
|
|
@@ -217,3 +412,6 @@ function readBoundedText(root, relativePath, sizeBytes) {
|
|
|
217
412
|
return null;
|
|
218
413
|
}
|
|
219
414
|
}
|
|
415
|
+
function isAndroidScopedSecurityValidationClaim(line) {
|
|
416
|
+
return /\bAndroid\b[^.\n]{0,60}\bsecurity validation\b/i.test(line) || /\bsecurity validation\b[^.\n]{0,30}\bfor Android\b/i.test(line);
|
|
417
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readBoundedFileText } from "../utils/boundedRead.js";
|
|
2
|
-
import { firstPathSegment,
|
|
2
|
+
import { firstPathSegment, GENERIC_INFRA_BASENAMES, baseNameNoExt, isGenericOrTestPrefixedDeclarationName, } from "../utils/filePatternUtils.js";
|
|
3
3
|
import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
|
|
4
4
|
import { indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
|
|
5
|
+
import { isJvmCommonLowSignalDeclarationName } from "../utils/jvmSourceFactsUtils.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// v0.3.0 Batch 4 — duplicate/parallel implementation candidate detector.
|
|
7
8
|
//
|
|
@@ -225,12 +226,12 @@ function firstTopLevelUnderSrc(relativePath) {
|
|
|
225
226
|
// (function/class/interface/type/enum) -- "variable"/"constant"/"method"/
|
|
226
227
|
// "unknown" are excluded as too noisy (methods repeat across unrelated
|
|
227
228
|
// classes by convention; module-scope variables/constants repeat even more
|
|
228
|
-
// often). Generic declaration names (run, init, handler,
|
|
229
|
-
//
|
|
230
|
-
// GENERIC_INFRA_BASENAMES excludes generic file basenames
|
|
231
|
-
// ctx.sourceFacts -- returns no issues when it is absent
|
|
232
|
-
// detector unit tests that build a literal
|
|
233
|
-
// it), so this check is purely additive.
|
|
229
|
+
// often). Generic/lifecycle declaration names (run, init, handler, __init__,
|
|
230
|
+
// test_*, ...) are excluded via isGenericOrTestPrefixedDeclarationName() for
|
|
231
|
+
// the same reason GENERIC_INFRA_BASENAMES excludes generic file basenames
|
|
232
|
+
// above. Requires ctx.sourceFacts -- returns no issues when it is absent
|
|
233
|
+
// (e.g. existing detector unit tests that build a literal
|
|
234
|
+
// AuditDetectorContext without it), so this check is purely additive.
|
|
234
235
|
const NOISY_DECLARATION_KINDS = new Set(["variable", "constant", "method", "unknown"]);
|
|
235
236
|
function findDuplicateDeclarationCandidates(ctx) {
|
|
236
237
|
if (!ctx.sourceFacts)
|
|
@@ -248,9 +249,41 @@ function findDuplicateDeclarationCandidates(ctx) {
|
|
|
248
249
|
if (NOISY_DECLARATION_KINDS.has(decl.kind))
|
|
249
250
|
continue;
|
|
250
251
|
const nameLower = decl.name.toLowerCase();
|
|
251
|
-
if (
|
|
252
|
+
if (isGenericOrTestPrefixedDeclarationName(nameLower))
|
|
252
253
|
continue;
|
|
253
|
-
|
|
254
|
+
// v0.3.3 Batch 2 -- Java/Kotlin-specific common/low-signal name
|
|
255
|
+
// suppression (Main, App, Config, Service, Controller, test*, ...).
|
|
256
|
+
// Gated to java/kotlin so existing TS/JS/Python behavior is
|
|
257
|
+
// unaffected -- a TS/JS file exporting `class Service {}` (a very
|
|
258
|
+
// ordinary, non-generic name in that ecosystem) must keep being
|
|
259
|
+
// considered, unlike the JVM ecosystem where these names are
|
|
260
|
+
// conventionally near-meaningless boilerplate labels.
|
|
261
|
+
if ((facts.language === "java" || facts.language === "kotlin") && isJvmCommonLowSignalDeclarationName(nameLower)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// v0.3.2 Batch 2 -- the key is scoped by `facts.analyzerId` (not
|
|
265
|
+
// `facts.language`) so a Python declaration is only ever compared
|
|
266
|
+
// against other Python declarations, never against a same-named
|
|
267
|
+
// TypeScript/JavaScript one. `analyzerId`, not `language`, is the
|
|
268
|
+
// right scope here: TypeScript and JavaScript are two different
|
|
269
|
+
// NormalizedLanguage values but share one analyzer
|
|
270
|
+
// (typescript-javascript-analyzer) and were already compared together
|
|
271
|
+
// before this batch -- scoping by `language` instead would have
|
|
272
|
+
// silently split that existing, still-desired TS/JS pairing in two
|
|
273
|
+
// (each side then below the "2+ files" threshold, so both would be
|
|
274
|
+
// dropped entirely), which is exactly the "preserve existing
|
|
275
|
+
// TypeScript/JavaScript duplicate behavior" regression this batch
|
|
276
|
+
// must not introduce. Scoping by `analyzerId` fixes the real
|
|
277
|
+
// cross-language bug (Python vs TS/JS) without touching TS/JS's own
|
|
278
|
+
// existing behavior at all. v0.3.3 Batch 2 -- this same scoping is
|
|
279
|
+
// why Java ("java-analyzer") and Kotlin ("kotlin-analyzer") never
|
|
280
|
+
// group with each other, TS/JS, or Python: they are two distinct
|
|
281
|
+
// analyzers with two distinct ids, deliberately not unified into one
|
|
282
|
+
// "JVM" scope in this batch (a same-named Java class and Kotlin
|
|
283
|
+
// class are not the same declaration in the same language, and
|
|
284
|
+
// conflating them risks a false "duplicate" signal across languages
|
|
285
|
+
// that don't even share syntax).
|
|
286
|
+
const key = `${facts.analyzerId}:${decl.kind}:${nameLower}`;
|
|
254
287
|
const entry = byKey.get(key) ?? { name: decl.name, kind: decl.kind, paths: new Set() };
|
|
255
288
|
entry.paths.add(facts.relativePath);
|
|
256
289
|
byKey.set(key, entry);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { collectPythonProjectMetadata } from "../../core/pythonProjectMetadata.js";
|
|
4
|
+
import { collectJvmProjectMetadata } from "../../core/jvmProjectMetadata.js";
|
|
3
5
|
import { readBoundedFileText } from "../utils/boundedRead.js";
|
|
6
|
+
import { baseNameNoExt } from "../utils/filePatternUtils.js";
|
|
4
7
|
import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
|
|
5
8
|
import { getParsedSourceFacts, indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
|
|
6
9
|
import { splitLines } from "../utils/textLines.js";
|
|
10
|
+
import { jvmImportCandidatePaths, jvmImportLooksPossiblyLocal, jvmImportSimpleName, } from "../utils/jvmSourceFactsUtils.js";
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
// v0.3.0 Batch 4 — test-rot detector.
|
|
9
13
|
//
|
|
@@ -44,6 +48,15 @@ export const TEST_ROT_DETECTOR = {
|
|
|
44
48
|
const scriptNames = new Set(ctx.sourceOfTruth.commands.allScriptNames);
|
|
45
49
|
const currentVersion = ctx.sourceOfTruth.package?.version ?? null;
|
|
46
50
|
const sourceFactsIndex = indexSourceFactsByPath(ctx.sourceFacts);
|
|
51
|
+
// v0.3.2 Batch 2 -- computed once per run, not per test file. Presence-
|
|
52
|
+
// only/simple-text-extraction (see pythonProjectMetadata.ts); used below
|
|
53
|
+
// purely as weak supporting context on Python findings, never as a
|
|
54
|
+
// required condition -- its absence must not change whether a finding
|
|
55
|
+
// is emitted, only add or omit one extra evidence line.
|
|
56
|
+
const pythonMetadata = collectPythonProjectMetadata(ctx.target.rootPath, ctx.inventory);
|
|
57
|
+
// v0.3.3 Batch 2 -- same "computed once, weak supporting context only"
|
|
58
|
+
// treatment as pythonMetadata above.
|
|
59
|
+
const jvmMetadata = collectJvmProjectMetadata(ctx.target.rootPath, ctx.inventory);
|
|
47
60
|
for (const testFile of ctx.inventory.testFiles) {
|
|
48
61
|
const content = readBoundedFileText(ctx.target.rootPath, testFile.relativePath, testFile.sizeBytes, MAX_READ_BYTES);
|
|
49
62
|
if (content === null)
|
|
@@ -71,7 +84,25 @@ export const TEST_ROT_DETECTOR = {
|
|
|
71
84
|
// (testRelativePath, "missing-source-import", specifier), so an
|
|
72
85
|
// identical id is deduplicated by deduplicateIssuesById() below.
|
|
73
86
|
const parsedFacts = getParsedSourceFacts(sourceFactsIndex, testFile.relativePath);
|
|
74
|
-
if (parsedFacts) {
|
|
87
|
+
if (parsedFacts?.language === "python") {
|
|
88
|
+
// v0.3.2 Batch 2 -- Python relative imports use dotted-module
|
|
89
|
+
// notation (".module", "..pkg.module"), not TS/JS's path-like
|
|
90
|
+
// "./module" syntax. Routing Python facts through the TS/JS
|
|
91
|
+
// resolver below would misresolve every relative Python import and
|
|
92
|
+
// flag it as missing regardless of whether it actually exists --
|
|
93
|
+
// this guard is the fix for that latent cross-language bug.
|
|
94
|
+
issues.push(...findMissingPythonSourceImports(ctx, testFile.relativePath, parsedFacts, pythonMetadata));
|
|
95
|
+
}
|
|
96
|
+
else if (parsedFacts?.language === "java" || parsedFacts?.language === "kotlin") {
|
|
97
|
+
// v0.3.3 Batch 2 -- Java/Kotlin imports are fully-qualified dotted
|
|
98
|
+
// package names ("com.example.Foo"), neither TS/JS's path-like
|
|
99
|
+
// "./module" syntax nor Python's dotted-relative-import syntax.
|
|
100
|
+
// Routing them through either existing resolver would misresolve
|
|
101
|
+
// every import -- this guard is the JVM analogue of the Python
|
|
102
|
+
// guard immediately above.
|
|
103
|
+
issues.push(...findMissingJvmSourceImports(ctx, testFile.relativePath, parsedFacts, jvmMetadata));
|
|
104
|
+
}
|
|
105
|
+
else if (parsedFacts) {
|
|
75
106
|
issues.push(...findMissingSourceImportsFromSourceFacts(ctx, testFile.relativePath, parsedFacts));
|
|
76
107
|
}
|
|
77
108
|
issues.push(...findStaleNpmRunReferences(testFile.relativePath, content, scriptNames));
|
|
@@ -177,6 +208,215 @@ function findMissingSourceImportsFromSourceFacts(ctx, testRelativePath, facts) {
|
|
|
177
208
|
}
|
|
178
209
|
return issues;
|
|
179
210
|
}
|
|
211
|
+
// v0.3.2 Batch 2 -- resolves a Python relative-import source (".module",
|
|
212
|
+
// "..pkg.sub", or bare "."/".." paired with `from . import name`-style
|
|
213
|
+
// imported names) to candidate local file paths, using Python's own
|
|
214
|
+
// relative-import level semantics (one leading dot = the importing file's
|
|
215
|
+
// own directory, each additional dot = one directory further up). This is a
|
|
216
|
+
// conservative, path-based approximation -- it does not resolve namespace
|
|
217
|
+
// packages, `sys.path` manipulation, editable installs, or anything beyond
|
|
218
|
+
// simple on-disk file/package presence, so returning zero matches is a
|
|
219
|
+
// candidate "may indicate a missing module" signal, not proof.
|
|
220
|
+
function resolvePythonRelativeImportCandidatePaths(testDir, importSource, importedNames) {
|
|
221
|
+
if (!importSource.startsWith("."))
|
|
222
|
+
return [];
|
|
223
|
+
let dotCount = 0;
|
|
224
|
+
while (dotCount < importSource.length && importSource[dotCount] === ".")
|
|
225
|
+
dotCount += 1;
|
|
226
|
+
const remainder = importSource.slice(dotCount);
|
|
227
|
+
const upLevels = dotCount - 1;
|
|
228
|
+
let baseDir = testDir;
|
|
229
|
+
for (let i = 0; i < upLevels; i++)
|
|
230
|
+
baseDir = path.dirname(baseDir);
|
|
231
|
+
const candidates = [];
|
|
232
|
+
const pushCandidatesFor = (moduleSegment) => {
|
|
233
|
+
candidates.push(path.join(baseDir, `${moduleSegment}.py`).replace(/\\/g, "/"));
|
|
234
|
+
candidates.push(path.join(baseDir, moduleSegment, "__init__.py").replace(/\\/g, "/"));
|
|
235
|
+
};
|
|
236
|
+
if (remainder) {
|
|
237
|
+
pushCandidatesFor(remainder.split(".").join("/"));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Bare "." / ".." (e.g. `from . import sibling`) -- the module lives
|
|
241
|
+
// in `importedNames`, not in `importSource` itself.
|
|
242
|
+
for (const name of importedNames ?? [])
|
|
243
|
+
pushCandidatesFor(name);
|
|
244
|
+
}
|
|
245
|
+
return candidates;
|
|
246
|
+
}
|
|
247
|
+
// v0.3.2 Batch 2 -- Python-specific counterpart to
|
|
248
|
+
// findMissingSourceImportsFromSourceFacts() above. Only ever called for
|
|
249
|
+
// facts with `language === "python"` (see caller) -- the two functions are
|
|
250
|
+
// kept fully separate rather than parameterized by language, because their
|
|
251
|
+
// resolution algorithms are genuinely different (dotted-module relative-
|
|
252
|
+
// import-level semantics vs. path-like specifier semantics), and conflating
|
|
253
|
+
// them risked silently misresolving one language or the other. Deliberately
|
|
254
|
+
// lower severity/confidence than the TS/JS counterpart: this scan has no
|
|
255
|
+
// real package/sys.path awareness, so it is weaker evidence than the
|
|
256
|
+
// TS/JS analyzer's near-exact relative-path resolution.
|
|
257
|
+
function findMissingPythonSourceImports(ctx, testRelativePath, facts, pythonMetadata) {
|
|
258
|
+
const issues = [];
|
|
259
|
+
const testDir = path.dirname(testRelativePath);
|
|
260
|
+
for (const imp of facts.imports) {
|
|
261
|
+
if (!imp.source.startsWith("."))
|
|
262
|
+
continue; // conservative: only relative Python imports are checked
|
|
263
|
+
const candidates = resolvePythonRelativeImportCandidatePaths(testDir, imp.source, imp.importedNames);
|
|
264
|
+
if (candidates.length === 0)
|
|
265
|
+
continue; // nothing checkable (e.g. bare "." with no imported names)
|
|
266
|
+
const resolved = candidates.some((candidate) => fs.existsSync(path.join(ctx.target.rootPath, candidate)));
|
|
267
|
+
if (resolved)
|
|
268
|
+
continue;
|
|
269
|
+
const evidence = [
|
|
270
|
+
{
|
|
271
|
+
kind: "file",
|
|
272
|
+
message: "Analyzer-recorded relative Python import does not resolve to a candidate local module/package file. This is a best-effort, path-based check -- it does not resolve Python's real package/sys.path rules, so confirm manually before treating it as stale.",
|
|
273
|
+
filePath: testRelativePath,
|
|
274
|
+
excerpt: imp.source,
|
|
275
|
+
source: DETECTOR_ID,
|
|
276
|
+
confidence: "low",
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
if (pythonMetadata.hasPytestConfiguration) {
|
|
280
|
+
evidence.push({
|
|
281
|
+
kind: "observation",
|
|
282
|
+
message: "Weak supporting context: this project has a detected pytest configuration (pytest.ini, or a pyproject.toml/setup.cfg pytest section), consistent with pytest-style test conventions -- not a requirement for this finding.",
|
|
283
|
+
source: DETECTOR_ID,
|
|
284
|
+
confidence: "low",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
issues.push(makeCodeRotIssue({
|
|
288
|
+
auditType: "code-rot",
|
|
289
|
+
detectorId: DETECTOR_ID,
|
|
290
|
+
idCues: [testRelativePath, "missing-python-source-import", imp.source],
|
|
291
|
+
title: `Test "${testRelativePath}" imports a Python module that may not exist: "${imp.source}"`,
|
|
292
|
+
description: `${testRelativePath} imports "${imp.source}" (source-facts-derived via the Python analyzer). No candidate local module or package file was found relative to the test file. This is a deterministic source-facts signal from a conservative, non-semantic scan -- it does not resolve Python's real package/sys.path rules, namespace packages, or editable installs, so this may indicate a stale import but is not proof.`,
|
|
293
|
+
severity: "low",
|
|
294
|
+
confidence: "low",
|
|
295
|
+
falsePositiveRisk: "medium",
|
|
296
|
+
category: "test-rot",
|
|
297
|
+
recommendedAction: "Confirm the imported Python module exists (it may resolve via a path this scan cannot see) before treating this as stale.",
|
|
298
|
+
suggestedFixStrategy: `Update or remove the import "${imp.source}" in ${testRelativePath} if confirmed stale.`,
|
|
299
|
+
validationCommands: [`npx vitest run ${testRelativePath}`],
|
|
300
|
+
releaseBlocking: false,
|
|
301
|
+
implementationBlocking: false,
|
|
302
|
+
autoFixEligible: false,
|
|
303
|
+
evidence,
|
|
304
|
+
affectedFiles: [testRelativePath],
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
return issues;
|
|
308
|
+
}
|
|
309
|
+
// v0.3.3 Batch 2 -- Java/Kotlin-specific counterpart to
|
|
310
|
+
// findMissingSourceImportsFromSourceFacts()/findMissingPythonSourceImports()
|
|
311
|
+
// above. Only ever called for facts with `language === "java"` or
|
|
312
|
+
// `"kotlin"` (see caller) -- kept fully separate from both existing
|
|
313
|
+
// resolvers for the same reason the Python resolver is kept separate from
|
|
314
|
+
// the TS/JS one: the resolution algorithm (fully-qualified dotted package
|
|
315
|
+
// names -> conventional Gradle/Maven source-set paths) is genuinely
|
|
316
|
+
// different from either.
|
|
317
|
+
//
|
|
318
|
+
// Deliberately lower severity/confidence than the TS/JS counterpart, same
|
|
319
|
+
// tier as the Python counterpart: this scan has no real classpath/build
|
|
320
|
+
// awareness (no Gradle/Maven execution, no compiler), so it is weaker
|
|
321
|
+
// evidence than the TS/JS analyzer's near-exact relative-path resolution.
|
|
322
|
+
// Standard-library, common third-party test-framework, and wildcard
|
|
323
|
+
// imports are skipped entirely via jvmImportLooksPossiblyLocal() -- see
|
|
324
|
+
// jvmSourceFactsUtils.ts for the full skip-prefix list and rationale.
|
|
325
|
+
function findMissingJvmSourceImports(ctx, testRelativePath, facts, jvmMetadata) {
|
|
326
|
+
const issues = [];
|
|
327
|
+
const isKotlin = facts.language === "kotlin";
|
|
328
|
+
const languageLabel = isKotlin ? "Kotlin" : "Java";
|
|
329
|
+
// Kotlin may legitimately import Java source in a mixed JVM project;
|
|
330
|
+
// Java never imports Kotlin-only source through a plain `import`
|
|
331
|
+
// statement in the way this conservative scan can see, so a Java test
|
|
332
|
+
// only ever checks .java candidates.
|
|
333
|
+
const extensions = isKotlin ? ["kt", "java"] : ["java"];
|
|
334
|
+
// Both recognized main and test source-set directories are checked --
|
|
335
|
+
// the latter specifically covers the "importing a test-scoped helper
|
|
336
|
+
// from another test-set file" case (the importing file here is, by
|
|
337
|
+
// definition, already test-scoped, since this function is only ever
|
|
338
|
+
// called from the per-test-file loop above).
|
|
339
|
+
const candidateDirs = [...jvmMetadata.recognizedSourceDirectories, ...jvmMetadata.recognizedTestDirectories];
|
|
340
|
+
const inventoryBasenameIndex = buildJvmInventoryBasenameIndex(ctx, extensions);
|
|
341
|
+
for (const imp of facts.imports) {
|
|
342
|
+
if (!jvmImportLooksPossiblyLocal(imp))
|
|
343
|
+
continue;
|
|
344
|
+
// A Java `import static com.example.Foo.bar;` (or a Kotlin top-level
|
|
345
|
+
// member import) has its *member* name as the last dotted segment, not
|
|
346
|
+
// a class name -- this analyzer's ImportFact model (shared across
|
|
347
|
+
// every language) does not distinguish "static import of a member"
|
|
348
|
+
// from "plain class import", so both interpretations are tried: the
|
|
349
|
+
// full dotted path as-is (the common, plain-class-import case), and
|
|
350
|
+
// with its last segment dropped (the static/member-import case). This
|
|
351
|
+
// is a detector-side fix, not a change to javaAnalyzer.ts/
|
|
352
|
+
// kotlinAnalyzer.ts's ImportFact shape.
|
|
353
|
+
const parentSource = imp.source.includes(".") ? imp.source.slice(0, imp.source.lastIndexOf(".")) : null;
|
|
354
|
+
const conventionalCandidates = [
|
|
355
|
+
...jvmImportCandidatePaths(imp.source, candidateDirs, extensions),
|
|
356
|
+
...(parentSource ? jvmImportCandidatePaths(parentSource, candidateDirs, extensions) : []),
|
|
357
|
+
];
|
|
358
|
+
const resolved = conventionalCandidates.some((candidate) => fs.existsSync(path.join(ctx.target.rootPath, candidate))) ||
|
|
359
|
+
inventoryBasenameIndex.has(jvmImportSimpleName(imp.source)) ||
|
|
360
|
+
(parentSource !== null && inventoryBasenameIndex.has(jvmImportSimpleName(parentSource)));
|
|
361
|
+
if (resolved)
|
|
362
|
+
continue;
|
|
363
|
+
const evidence = [
|
|
364
|
+
{
|
|
365
|
+
kind: "file",
|
|
366
|
+
message: `Analyzer-recorded ${languageLabel} import does not resolve to a candidate local source file under a recognized source-set directory, nor to any inventoried file with a matching basename. This is a best-effort, path-based check -- no compiler/classpath analysis was performed, and no Gradle/Maven execution was performed, so confirm manually before treating it as stale.`,
|
|
367
|
+
filePath: testRelativePath,
|
|
368
|
+
excerpt: imp.source,
|
|
369
|
+
source: DETECTOR_ID,
|
|
370
|
+
confidence: "low",
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
if (candidateDirs.length > 0) {
|
|
374
|
+
evidence.push({
|
|
375
|
+
kind: "observation",
|
|
376
|
+
message: `Weak supporting context: recognized source-set directories for this project are: ${candidateDirs.join(", ")}.`,
|
|
377
|
+
source: DETECTOR_ID,
|
|
378
|
+
confidence: "low",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
issues.push(makeCodeRotIssue({
|
|
382
|
+
auditType: "code-rot",
|
|
383
|
+
detectorId: DETECTOR_ID,
|
|
384
|
+
idCues: [testRelativePath, `missing-${facts.language}-source-import`, imp.source],
|
|
385
|
+
title: `Test "${testRelativePath}" imports a ${languageLabel} class that may not exist: "${imp.source}"`,
|
|
386
|
+
description: `${testRelativePath} imports "${imp.source}" (source-facts-derived via the ${languageLabel} analyzer). No candidate local source file was found under a recognized source-set directory, and no inventoried file shares its simple name. This is a deterministic source-facts signal from a conservative, non-semantic scan -- no compiler/classpath analysis was performed, and no Gradle/Maven execution was performed, so this does not resolve the real classpath, package visibility, or multi-module project layout. It may indicate a stale import but is not proof.`,
|
|
387
|
+
severity: "low",
|
|
388
|
+
confidence: "low",
|
|
389
|
+
falsePositiveRisk: "medium",
|
|
390
|
+
category: "test-rot",
|
|
391
|
+
recommendedAction: "Confirm the imported class exists (it may resolve via a module/classpath this scan cannot see) before treating this as stale.",
|
|
392
|
+
suggestedFixStrategy: `Update or remove the import "${imp.source}" in ${testRelativePath} if confirmed stale.`,
|
|
393
|
+
validationCommands: [`npx vitest run ${testRelativePath}`],
|
|
394
|
+
releaseBlocking: false,
|
|
395
|
+
implementationBlocking: false,
|
|
396
|
+
autoFixEligible: false,
|
|
397
|
+
evidence,
|
|
398
|
+
affectedFiles: [testRelativePath],
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
return issues;
|
|
402
|
+
}
|
|
403
|
+
// Lowercased-basename index (without extension) of every inventoried
|
|
404
|
+
// Java/Kotlin file matching the given extensions -- the lenient,
|
|
405
|
+
// last-resort fallback used by findMissingJvmSourceImports() above so a
|
|
406
|
+
// real (if unconventionally placed) local file is never flagged as
|
|
407
|
+
// missing merely because the project doesn't follow the conventional
|
|
408
|
+
// src/main/{java,kotlin} source-set layout (mirrors
|
|
409
|
+
// jvmProjectMetadata.ts's own "non-conventional layout" reasoning).
|
|
410
|
+
function buildJvmInventoryBasenameIndex(ctx, extensions) {
|
|
411
|
+
const extSet = new Set(extensions.map((e) => `.${e}`));
|
|
412
|
+
const basenames = new Set();
|
|
413
|
+
for (const file of ctx.inventory.files) {
|
|
414
|
+
if (!extSet.has(file.extension))
|
|
415
|
+
continue;
|
|
416
|
+
basenames.add(baseNameNoExt(file.relativePath));
|
|
417
|
+
}
|
|
418
|
+
return basenames;
|
|
419
|
+
}
|
|
180
420
|
function findStaleNpmRunReferences(testRelativePath, content, scriptNames) {
|
|
181
421
|
const issues = [];
|
|
182
422
|
const seen = new Set();
|
|
@@ -49,13 +49,17 @@ export const GENERIC_INFRA_BASENAMES = new Set([
|
|
|
49
49
|
// expected to repeat across unrelated files legitimately (CLI entrypoints,
|
|
50
50
|
// handler/lifecycle conventions, etc.) -- excluded from the source-facts
|
|
51
51
|
// duplicate-declaration-candidate heuristic in duplicateImplementationDetector.ts
|
|
52
|
-
// to keep false positives near zero.
|
|
52
|
+
// to keep false positives near zero. v0.3.2 Batch 2 adds Python lifecycle/
|
|
53
|
+
// dunder names ("teardown", "__init__", "__call__") for the same reason,
|
|
54
|
+
// reused by both duplicateImplementationDetector.ts and
|
|
55
|
+
// deadCodeCandidateDetector.ts's new Python-aware checks.
|
|
53
56
|
export const GENERIC_DECLARATION_NAMES = new Set([
|
|
54
57
|
"run",
|
|
55
58
|
"main",
|
|
56
59
|
"init",
|
|
57
60
|
"initialize",
|
|
58
61
|
"setup",
|
|
62
|
+
"teardown",
|
|
59
63
|
"handler",
|
|
60
64
|
"handle",
|
|
61
65
|
"execute",
|
|
@@ -76,4 +80,18 @@ export const GENERIC_DECLARATION_NAMES = new Set([
|
|
|
76
80
|
"get",
|
|
77
81
|
"set",
|
|
78
82
|
"default",
|
|
83
|
+
"__init__",
|
|
84
|
+
"__call__",
|
|
79
85
|
]);
|
|
86
|
+
// v0.3.2 Batch 2 -- true for a generic/lifecycle declaration name (see
|
|
87
|
+
// GENERIC_DECLARATION_NAMES above) or a pytest-style `test_*` name. Shared by
|
|
88
|
+
// the Python-aware dead-code and duplicate-implementation checks so a
|
|
89
|
+
// `test_*` helper/fixture function is never flagged as a candidate dead-code
|
|
90
|
+
// symbol or a duplicate-implementation candidate merely for repeating across
|
|
91
|
+
// unrelated test modules, a legitimate and extremely common pytest
|
|
92
|
+
// convention. Comparison is case-insensitive; callers pass the already-
|
|
93
|
+
// lowercased name where convenient.
|
|
94
|
+
export function isGenericOrTestPrefixedDeclarationName(name) {
|
|
95
|
+
const lower = name.toLowerCase();
|
|
96
|
+
return GENERIC_DECLARATION_NAMES.has(lower) || lower.startsWith("test_");
|
|
97
|
+
}
|