@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.3.3 Batch 2 -- shared JVM (Java/Kotlin) source-facts helpers reused by
|
|
3
|
+
// deadCodeCandidateDetector.ts, duplicateImplementationDetector.ts, and
|
|
4
|
+
// testRotDetector.ts.
|
|
5
|
+
//
|
|
6
|
+
// Deterministic and dependency-free: no Gradle/Maven execution, no
|
|
7
|
+
// classpath resolution, no Java/Kotlin compiler, no traversal outside the
|
|
8
|
+
// target root. Everything here operates purely on already-collected
|
|
9
|
+
// SourceFacts/inventory data (see javaAnalyzer.ts/kotlinAnalyzer.ts/
|
|
10
|
+
// jvmProjectMetadata.ts from v0.3.3 Batch 1).
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Import prefixes considered standard-library or common third-party test
|
|
13
|
+
// framework noise -- never treated as "possibly a local project import" by
|
|
14
|
+
// testRotDetector.ts's JVM resolver, regardless of whether a local file
|
|
15
|
+
// happens to share a path segment. Matched against the import source's
|
|
16
|
+
// leading dotted segments so "java.util.List" matches "java." but a
|
|
17
|
+
// hypothetical local package literally named "javautils.Foo" (no dot after
|
|
18
|
+
// "java") would not.
|
|
19
|
+
const JVM_SKIPPABLE_IMPORT_PREFIXES = [
|
|
20
|
+
"java.",
|
|
21
|
+
"javax.",
|
|
22
|
+
"jakarta.",
|
|
23
|
+
"kotlin.",
|
|
24
|
+
"kotlinx.",
|
|
25
|
+
"org.junit.",
|
|
26
|
+
"org.assertj.",
|
|
27
|
+
"org.mockito.",
|
|
28
|
+
"org.hamcrest.",
|
|
29
|
+
"io.kotest.",
|
|
30
|
+
];
|
|
31
|
+
export function isJvmSkippableImportSource(source) {
|
|
32
|
+
return JVM_SKIPPABLE_IMPORT_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
33
|
+
}
|
|
34
|
+
// True for a wildcard-style import (`import com.example.*` or, for Java
|
|
35
|
+
// static imports, `import static com.example.Foo.*`). Deliberately treated
|
|
36
|
+
// as never "safely local" by testRotDetector.ts's JVM resolver -- there is
|
|
37
|
+
// no way to derive a single candidate file from a wildcard without real
|
|
38
|
+
// classpath/package-member awareness, so guessing would either miss real
|
|
39
|
+
// stale imports (if resolved unconditionally) or produce high-noise false
|
|
40
|
+
// positives (if flagged unconditionally). Skipping entirely is the
|
|
41
|
+
// conservative choice.
|
|
42
|
+
export function isJvmWildcardImportSource(source) {
|
|
43
|
+
return source.endsWith(".*");
|
|
44
|
+
}
|
|
45
|
+
// The simple (unqualified) name a dotted JVM import source resolves to --
|
|
46
|
+
// the last segment after the final ".". For a Java static import
|
|
47
|
+
// (`static com.example.Foo.bar`), this is the imported static member name,
|
|
48
|
+
// not the enclosing class -- an accepted, documented conflation for this
|
|
49
|
+
// conservative, best-effort scan (see javaAnalyzer.ts's own header comment
|
|
50
|
+
// on why `import static` is not modeled any more precisely than a plain
|
|
51
|
+
// import).
|
|
52
|
+
export function jvmImportSimpleName(source) {
|
|
53
|
+
const idx = source.lastIndexOf(".");
|
|
54
|
+
return idx === -1 ? source : source.slice(idx + 1);
|
|
55
|
+
}
|
|
56
|
+
// Converts a dotted JVM import source (e.g. "com.example.Foo") into a
|
|
57
|
+
// package-shaped relative path segment (e.g. "com/example/Foo"), dropping
|
|
58
|
+
// any trailing wildcard/member segment handling -- callers are responsible
|
|
59
|
+
// for skipping wildcards via isJvmWildcardImportSource() first.
|
|
60
|
+
export function jvmImportToPackagePath(source) {
|
|
61
|
+
return source.split(".").join("/");
|
|
62
|
+
}
|
|
63
|
+
// Candidate on-disk relative paths for a (non-wildcard, non-skippable)
|
|
64
|
+
// import, given the project's recognized Java/Kotlin source-set
|
|
65
|
+
// directories (see jvmProjectMetadata.ts's recognizedSourceDirectories).
|
|
66
|
+
// Conservative and path-based only: this never resolves the real classpath,
|
|
67
|
+
// only checks the conventional Gradle/Maven `src/main/{java,kotlin}/...`
|
|
68
|
+
// layout. `extensions` lets a Kotlin importer also check `.java` (Kotlin
|
|
69
|
+
// can legitimately import Java source in a mixed JVM project) while a Java
|
|
70
|
+
// importer only ever checks `.java`.
|
|
71
|
+
export function jvmImportCandidatePaths(source, recognizedSourceDirectories, extensions) {
|
|
72
|
+
const packagePath = jvmImportToPackagePath(source);
|
|
73
|
+
const candidates = [];
|
|
74
|
+
for (const dir of recognizedSourceDirectories) {
|
|
75
|
+
for (const ext of extensions) {
|
|
76
|
+
candidates.push(`${dir}/${packagePath}.${ext}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return candidates;
|
|
80
|
+
}
|
|
81
|
+
// True when an ImportFact looks worth checking for local-file existence at
|
|
82
|
+
// all -- i.e. not a standard-library/common-test-framework import and not a
|
|
83
|
+
// wildcard. Callers still need their own existence check; this only
|
|
84
|
+
// filters out imports this conservative scan should never second-guess.
|
|
85
|
+
export function jvmImportLooksPossiblyLocal(imp) {
|
|
86
|
+
if (isJvmWildcardImportSource(imp.source))
|
|
87
|
+
return false;
|
|
88
|
+
if (isJvmSkippableImportSource(imp.source))
|
|
89
|
+
return false;
|
|
90
|
+
// A bare, undotted import (e.g. a same-package reference, or a malformed
|
|
91
|
+
// scan artifact) has no package path to resolve against -- not
|
|
92
|
+
// checkable by this path-based resolver.
|
|
93
|
+
if (!imp.source.includes("."))
|
|
94
|
+
return false;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Declaration-name conservatism helpers.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Common JVM class/type names that legitimately repeat across unrelated
|
|
101
|
+
// projects/modules (a "Main" class, an "App" entrypoint, a "Config" class
|
|
102
|
+
// per module, etc.) -- excluded from duplicateImplementationDetector.ts's
|
|
103
|
+
// JVM duplicate-declaration-candidate check the same way
|
|
104
|
+
// GENERIC_DECLARATION_NAMES/GENERIC_INFRA_BASENAMES already exclude their
|
|
105
|
+
// TS/JS/Python/file-basename equivalents.
|
|
106
|
+
const JVM_COMMON_TYPE_NAMES = new Set([
|
|
107
|
+
"main",
|
|
108
|
+
"app",
|
|
109
|
+
"application",
|
|
110
|
+
"config",
|
|
111
|
+
"configuration",
|
|
112
|
+
"utils",
|
|
113
|
+
"helper",
|
|
114
|
+
"factory",
|
|
115
|
+
"builder",
|
|
116
|
+
"manager",
|
|
117
|
+
"service",
|
|
118
|
+
"controller",
|
|
119
|
+
"repository",
|
|
120
|
+
]);
|
|
121
|
+
export function isJvmCommonLowSignalDeclarationName(name) {
|
|
122
|
+
const lower = name.toLowerCase();
|
|
123
|
+
if (JVM_COMMON_TYPE_NAMES.has(lower))
|
|
124
|
+
return true;
|
|
125
|
+
if (lower.startsWith("test"))
|
|
126
|
+
return true;
|
|
127
|
+
return lower === "setup" || lower === "teardown";
|
|
128
|
+
}
|
|
129
|
+
// Java/Kotlin lifecycle, `Object`-override, and bean-accessor-shaped names
|
|
130
|
+
// -- excluded from deadCodeCandidateDetector.ts's JVM dead-code-candidate
|
|
131
|
+
// check, the same way isGenericOrTestPrefixedDeclarationName() already
|
|
132
|
+
// excludes TS/JS/Python's equivalents. `get*`/`set*`/`is*` in particular
|
|
133
|
+
// cover the extremely common JavaBean accessor convention, where a
|
|
134
|
+
// "no detected cross-file reference" signal is essentially meaningless
|
|
135
|
+
// (framework/reflection-based access is invisible to this static scan).
|
|
136
|
+
const JVM_LIFECYCLE_EXACT_NAMES = new Set([
|
|
137
|
+
"main",
|
|
138
|
+
"tostring",
|
|
139
|
+
"equals",
|
|
140
|
+
"hashcode",
|
|
141
|
+
"compareto",
|
|
142
|
+
"clone",
|
|
143
|
+
"setup",
|
|
144
|
+
"teardown",
|
|
145
|
+
"copy", // Kotlin data class synthesized-like member
|
|
146
|
+
]);
|
|
147
|
+
const JVM_LIFECYCLE_PREFIXES = ["get", "set", "is", "can", "should", "before", "after", "test", "component"];
|
|
148
|
+
export function isJvmLifecycleOrConventionalDeclarationName(name) {
|
|
149
|
+
const lower = name.toLowerCase();
|
|
150
|
+
if (JVM_LIFECYCLE_EXACT_NAMES.has(lower))
|
|
151
|
+
return true;
|
|
152
|
+
return JVM_LIFECYCLE_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
153
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { AUDIT_FAIL_ON_THRESHOLDS, AUDIT_INCLUDE_AREAS, AUDIT_OUTPUT_FORMATS, AUDIT_TYPES, DEFAULT_AUDIT_FAIL_ON_THRESHOLD, DEFAULT_AUDIT_INCLUDE_AREAS, DEFAULT_AUDIT_OUTPUT_FORMATS, DEFAULT_AUDIT_TYPES, PLANNED_AUDIT_TYPES, } from "./auditTypes.js";
|
|
2
|
+
import { AUDIT_FAIL_ON_THRESHOLDS, AUDIT_INCLUDE_AREAS, AUDIT_OUTPUT_FORMATS, AUDIT_TYPES, DEFAULT_AUDIT_FAIL_ON_THRESHOLD, DEFAULT_AUDIT_INCLUDE_AREAS, DEFAULT_AUDIT_OUTPUT_FORMATS, DEFAULT_AUDIT_TYPES, IMPLEMENTED_AUDIT_TYPES, PLANNED_AUDIT_TYPES, } from "./auditTypes.js";
|
|
3
3
|
const FLAGS_WITH_VALUE = {
|
|
4
4
|
"--target": "target",
|
|
5
5
|
"--types": "types",
|
|
@@ -67,7 +67,7 @@ function parseTypesOption(raw) {
|
|
|
67
67
|
}
|
|
68
68
|
const planned = parts.filter((p) => PLANNED_AUDIT_TYPES.includes(p));
|
|
69
69
|
if (planned.length > 0) {
|
|
70
|
-
throw new Error(`--types value(s) planned but not implemented: ${planned.join(", ")}.
|
|
70
|
+
throw new Error(`--types value(s) planned but not implemented: ${planned.join(", ")}. Implemented audit types: ${IMPLEMENTED_AUDIT_TYPES.join(", ")}.`);
|
|
71
71
|
}
|
|
72
72
|
const unique = new Set(parts);
|
|
73
73
|
const ordered = AUDIT_TYPES.filter((id) => unique.has(id));
|
|
@@ -4,6 +4,9 @@ import { resolveAuditTarget } from "./auditTarget.js";
|
|
|
4
4
|
import { scanProjectInventory } from "./projectInventory.js";
|
|
5
5
|
import { collectSourceOfTruth } from "./sourceOfTruth.js";
|
|
6
6
|
import { collectSourceFacts } from "./collectSourceFacts.js";
|
|
7
|
+
import { collectPythonProjectMetadata } from "./pythonProjectMetadata.js";
|
|
8
|
+
import { runSecurityAuditAdapter } from "../security/securityAuditAdapter.js";
|
|
9
|
+
import { SECURITY_AUDIT_NOT_RUN_SUMMARY } from "../security/securityAuditTypes.js";
|
|
7
10
|
export async function runAudit(options) {
|
|
8
11
|
const { config, toolRoot } = options;
|
|
9
12
|
const registry = options.registry ?? DEFAULT_AUDIT_REGISTRY;
|
|
@@ -11,6 +14,7 @@ export async function runAudit(options) {
|
|
|
11
14
|
const inventory = scanProjectInventory(target.rootPath);
|
|
12
15
|
const sourceOfTruth = collectSourceOfTruth(target.rootPath, inventory);
|
|
13
16
|
const sourceFacts = await collectSourceFacts(target.rootPath, inventory);
|
|
17
|
+
const pythonProjectMetadata = collectPythonProjectMetadata(target.rootPath, inventory);
|
|
14
18
|
const selected = selectDetectors(registry, config.types, config.include);
|
|
15
19
|
const ctx = { target, config, inventory, sourceOfTruth, sourceFacts };
|
|
16
20
|
const issues = [];
|
|
@@ -37,6 +41,19 @@ export async function runAudit(options) {
|
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
// v0.3.2 Batch 4 -- security audit adapter. Conditional (see the
|
|
45
|
+
// AuditResult.securitySummary field comment above): only runs when --types
|
|
46
|
+
// explicitly includes "security", never as part of a default/code-rot-only
|
|
47
|
+
// run. Appended after the code-rot detector loop above, so combined
|
|
48
|
+
// `--types code-rot,security` runs keep a deterministic issue order:
|
|
49
|
+
// code-rot detector issues (registry order) first, then security findings
|
|
50
|
+
// (security-validation check-execution order).
|
|
51
|
+
let securitySummary = SECURITY_AUDIT_NOT_RUN_SUMMARY;
|
|
52
|
+
if (config.types.includes("security")) {
|
|
53
|
+
const securityAudit = await runSecurityAuditAdapter({ toolRoot, config });
|
|
54
|
+
issues.push(...securityAudit.issues);
|
|
55
|
+
securitySummary = securityAudit.summary;
|
|
56
|
+
}
|
|
40
57
|
const issueCounts = countIssuesBySeverity(issues);
|
|
41
58
|
const { exitCode, reason } = calculateAuditExitCode(issues, config.failOn);
|
|
42
59
|
return {
|
|
@@ -62,6 +79,8 @@ export async function runAudit(options) {
|
|
|
62
79
|
inventory,
|
|
63
80
|
sourceOfTruth,
|
|
64
81
|
sourceFacts,
|
|
82
|
+
pythonProjectMetadata,
|
|
83
|
+
securitySummary,
|
|
65
84
|
noDetectorsRegistered: selected.length === 0,
|
|
66
85
|
};
|
|
67
86
|
}
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
// this batch keeps the audit issue schema independent even where a
|
|
8
8
|
// vocabulary word (e.g. "blocker") happens to be shared.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
// Recognized --types values.
|
|
11
|
-
// Batch
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Recognized --types values. "code-rot" (Batch 1) and "security" (v0.3.2
|
|
11
|
+
// Batch 4, via the security-validation audit adapter) have real coverage —
|
|
12
|
+
// the rest are known, named future audit types that must fail cleanly with a
|
|
13
|
+
// "planned but not implemented" message rather than being silently accepted
|
|
14
|
+
// or treated as an unknown/garbage value.
|
|
14
15
|
export const AUDIT_TYPES = ["code-rot", "quality", "security", "project", "all"];
|
|
15
|
-
export const IMPLEMENTED_AUDIT_TYPES = ["code-rot"];
|
|
16
|
-
export const PLANNED_AUDIT_TYPES = ["quality", "
|
|
17
|
-
|
|
16
|
+
export const IMPLEMENTED_AUDIT_TYPES = ["code-rot", "security"];
|
|
17
|
+
export const PLANNED_AUDIT_TYPES = ["quality", "project", "all"];
|
|
18
|
+
// v0.3.2 Batch 4 — deliberately NOT derived from IMPLEMENTED_AUDIT_TYPES.
|
|
19
|
+
// "security" becomes selectable via --types security (and --types
|
|
20
|
+
// code-rot,security) but must never join the *default* (no --types flag)
|
|
21
|
+
// audit run, since it shells out to npm audit/CodeQL/Semgrep/fuzz and is far
|
|
22
|
+
// more expensive than the always-on code-rot detectors — a default,
|
|
23
|
+
// unqualified `npm run audit` must keep its existing code-rot-only behavior.
|
|
24
|
+
export const DEFAULT_AUDIT_TYPES = ["code-rot"];
|
|
18
25
|
export const AUDIT_INCLUDE_AREAS = ["docs", "tests", "package", "architecture", "cli"];
|
|
19
26
|
export const DEFAULT_AUDIT_INCLUDE_AREAS = AUDIT_INCLUDE_AREAS;
|
|
20
27
|
export const AUDIT_OUTPUT_FORMATS = ["text", "json"];
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.3.3 Batch 1 -- conservative, dependency-free Java source analyzer.
|
|
3
|
+
//
|
|
4
|
+
// There is no Java compiler/parser available in this Node/TypeScript
|
|
5
|
+
// project, and per this batch's scope this analyzer must not add a new
|
|
6
|
+
// parser dependency (no javac, no javaparser-style library). Facts are
|
|
7
|
+
// extracted with a deliberately narrow, brace-depth-tracking line scan --
|
|
8
|
+
// not a real tokenizer, not an AST, and not a syntax validator. Same
|
|
9
|
+
// LanguageAnalyzer contract, same SourceFileFacts shape, and the same
|
|
10
|
+
// size-bounding philosophy as typescriptJavaScriptAnalyzer.ts/
|
|
11
|
+
// pythonAnalyzer.ts, but confidence is necessarily lower since nothing here
|
|
12
|
+
// proves the file is syntactically valid Java.
|
|
13
|
+
//
|
|
14
|
+
// What this DOES extract:
|
|
15
|
+
// - a single `package foo.bar;` declaration
|
|
16
|
+
// - `import foo.bar.Baz;` and `import static foo.bar.Baz.member;` (both
|
|
17
|
+
// recorded via the existing generic ImportFactKind "static" -- the same
|
|
18
|
+
// "resolved through a static, syntactic import statement" meaning
|
|
19
|
+
// already used by every other analyzer in this project, not a claim
|
|
20
|
+
// about Java's specific "import static" keyword)
|
|
21
|
+
// - top-level and nested class/interface/enum declarations
|
|
22
|
+
// - method and constructor declarations directly inside a class/
|
|
23
|
+
// interface/enum body (both recorded as DeclarationFactKind "method" --
|
|
24
|
+
// there is no dedicated constructor kind in the shared model, and
|
|
25
|
+
// "method" is a safe, non-overclaiming fit)
|
|
26
|
+
// - visibility (public/protected/private/package-private), folded into
|
|
27
|
+
// the existing `exported` boolean: `exported: true` only for an
|
|
28
|
+
// explicit `public` modifier, `false` for protected/private/
|
|
29
|
+
// package-private -- see "Visibility mapping" below for why no new
|
|
30
|
+
// schema field was added
|
|
31
|
+
// - line counts, parse/analyze status, and diagnostics for degraded
|
|
32
|
+
// parsing
|
|
33
|
+
//
|
|
34
|
+
// What this explicitly does NOT do: real Java compilation, type resolution,
|
|
35
|
+
// symbol/overload resolution, inheritance analysis, annotation/framework
|
|
36
|
+
// lifecycle understanding, runtime behavior, Gradle/Maven source-set
|
|
37
|
+
// validation, semantic duplicate-implementation proof, or dead-code proof.
|
|
38
|
+
// Field/property declarations are not extracted (out of this batch's
|
|
39
|
+
// required scope, and harder to distinguish conservatively from local
|
|
40
|
+
// variable declarations via regex alone). Java text blocks (`"""..."""`,
|
|
41
|
+
// Java 15+) are not given special multi-line handling the way Kotlin's raw
|
|
42
|
+
// strings are in kotlinAnalyzer.ts -- if present, a `{`/`}`/quote character
|
|
43
|
+
// inside one could be slightly miscounted, which is an accepted, narrow
|
|
44
|
+
// limitation of a regex/brace-counting scanner rather than a crash risk.
|
|
45
|
+
//
|
|
46
|
+
// Visibility mapping (no SourceFacts schema change):
|
|
47
|
+
// DeclarationFact has no dedicated visibility field, and per this batch's
|
|
48
|
+
// "prefer no schema change" guidance, none is added here. Java's four
|
|
49
|
+
// visibility levels collapse onto the existing boolean `exported` the
|
|
50
|
+
// same way the Python analyzer already folds its own export heuristic
|
|
51
|
+
// into that field: `public` -> true, everything else (protected/private/
|
|
52
|
+
// package-private) -> false. This intentionally loses the
|
|
53
|
+
// protected-vs-private-vs-package-private distinction; callers that need
|
|
54
|
+
// finer detail must re-derive it from source, which this analyzer does
|
|
55
|
+
// not claim to make unnecessary.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
export const JAVA_ANALYZER_ID = "java-analyzer";
|
|
58
|
+
const SUPPORTED_EXTENSIONS = [".java"];
|
|
59
|
+
// Mirrors typescriptJavaScriptAnalyzer.ts/pythonAnalyzer.ts's bound.
|
|
60
|
+
const MAX_ANALYZABLE_BYTES = 1_000_000;
|
|
61
|
+
const PACKAGE_LINE_PATTERN = /^\s*package\s+([\w.]+)\s*;/;
|
|
62
|
+
const IMPORT_LINE_PATTERN = /^\s*import\s+(static\s+)?([\w.]+(?:\.\*)?)\s*;/;
|
|
63
|
+
// Requires an explicit visibility modifier to be captured separately so
|
|
64
|
+
// "no modifier present" (package-private) is distinguishable from an
|
|
65
|
+
// explicit one -- see MODIFIER_PATTERN below.
|
|
66
|
+
const MODIFIER_WORDS = "(?:public|protected|private|static|final|abstract|sealed|non-sealed|strictfp|synchronized|native|default)";
|
|
67
|
+
const TYPE_DECL_PATTERN = new RegExp(`^\\s*(?:${MODIFIER_WORDS}\\s+)*(class|interface|enum|record)\\s+(\\w+)`);
|
|
68
|
+
// Two separate word-tokens (a return-type-like token, then the method name)
|
|
69
|
+
// before the parameter list -- this is what keeps `if (...)`, `for (...)`,
|
|
70
|
+
// `while (...)`, `switch (...)`, and plain method-call statements
|
|
71
|
+
// (`foo.bar(x);`, a single dotted token with no preceding space) from ever
|
|
72
|
+
// matching: those all have only one space-separated token before `(`.
|
|
73
|
+
// The trailing group accepts either just an opening brace (multi-line
|
|
74
|
+
// body), a semicolon (abstract/interface method declaration), or a full
|
|
75
|
+
// one-liner body with no nested braces (e.g. `{ return true; }`, `{}`) --
|
|
76
|
+
// without the last alternative, a common one-liner accessor/getter would
|
|
77
|
+
// never be recognized as a declaration at all.
|
|
78
|
+
const METHOD_LINE_PATTERN = new RegExp(`^\\s*(?:${MODIFIER_WORDS}\\s+)*[\\w<>\\[\\],.?]+\\s+(\\w+)\\s*\\(([^()]*)\\)\\s*(?:throws\\s+[\\w.,\\s<>]+)?\\s*(?:\\{[^{}]*\\}|\\{|;)\\s*$`);
|
|
79
|
+
// A constructor has no return-type token: just an optional visibility
|
|
80
|
+
// modifier, the identifier, then the parameter list. The caller only
|
|
81
|
+
// accepts a match here when the identifier equals the enclosing class/
|
|
82
|
+
// enum/record name, which is what actually distinguishes a real
|
|
83
|
+
// constructor from a stray call-like statement (see analyzeFile below).
|
|
84
|
+
const CONSTRUCTOR_LINE_PATTERN = /^\s*(?:(?:public|protected|private)\s+)?(\w+)\s*\(([^()]*)\)\s*(?:throws\s+[\w.,\s<>]+)?\s*(?:\{[^{}]*\}|\{)\s*$/;
|
|
85
|
+
const EXPLICIT_MODIFIER_PATTERN = /(?:^|\s)(public|protected|private)(?:\s|$)/;
|
|
86
|
+
export const JAVA_ANALYZER = {
|
|
87
|
+
id: JAVA_ANALYZER_ID,
|
|
88
|
+
supportedLanguages: ["java"],
|
|
89
|
+
supportedExtensions: SUPPORTED_EXTENSIONS,
|
|
90
|
+
analyzeFile,
|
|
91
|
+
};
|
|
92
|
+
function analyzeFile(input) {
|
|
93
|
+
const { relativePath, language, role, content, inventoryEntry } = input;
|
|
94
|
+
if (Buffer.byteLength(content, "utf8") > MAX_ANALYZABLE_BYTES) {
|
|
95
|
+
return {
|
|
96
|
+
relativePath,
|
|
97
|
+
language,
|
|
98
|
+
role,
|
|
99
|
+
parseStatus: "file-level-only",
|
|
100
|
+
analyzerId: JAVA_ANALYZER_ID,
|
|
101
|
+
lineCount: inventoryEntry.lineCount,
|
|
102
|
+
imports: [],
|
|
103
|
+
exports: [],
|
|
104
|
+
declarations: [],
|
|
105
|
+
references: [],
|
|
106
|
+
diagnostics: [
|
|
107
|
+
{
|
|
108
|
+
severity: "info",
|
|
109
|
+
code: "file-too-large-for-analysis",
|
|
110
|
+
message: `File exceeds the ${MAX_ANALYZABLE_BYTES}-byte analyzer bound; only inventory-level facts are available.`,
|
|
111
|
+
path: relativePath,
|
|
112
|
+
analyzerId: JAVA_ANALYZER_ID,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const diagnostics = [];
|
|
118
|
+
const { logical, hadUnterminatedBlockComment } = buildLogicalLines(content);
|
|
119
|
+
const imports = [];
|
|
120
|
+
const declarations = [];
|
|
121
|
+
const scopeStack = [];
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let packageSeen = false;
|
|
124
|
+
for (let i = 0; i < logical.length; i++) {
|
|
125
|
+
const line = logical[i];
|
|
126
|
+
const lineNumber = i + 1;
|
|
127
|
+
if (line.trim().length === 0) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!packageSeen) {
|
|
131
|
+
const packageMatch = line.match(PACKAGE_LINE_PATTERN);
|
|
132
|
+
if (packageMatch)
|
|
133
|
+
packageSeen = true;
|
|
134
|
+
}
|
|
135
|
+
const importMatch = line.match(IMPORT_LINE_PATTERN);
|
|
136
|
+
if (importMatch) {
|
|
137
|
+
imports.push({ source: importMatch[2], kind: "static", line: lineNumber });
|
|
138
|
+
}
|
|
139
|
+
const typeMatch = line.match(TYPE_DECL_PATTERN);
|
|
140
|
+
// "record" (a restricted, implicitly-final class variant) is folded
|
|
141
|
+
// into the existing "class" DeclarationFactKind/ScopeKind -- there is
|
|
142
|
+
// no dedicated record kind in the shared model, and "class" is a safe,
|
|
143
|
+
// non-overclaiming fit (a record's synthesized accessor/constructor
|
|
144
|
+
// methods are not separately modeled).
|
|
145
|
+
const normalizedKind = typeMatch ? (typeMatch[1] === "record" ? "class" : typeMatch[1]) : null;
|
|
146
|
+
if (typeMatch && normalizedKind) {
|
|
147
|
+
const name = typeMatch[2];
|
|
148
|
+
declarations.push({
|
|
149
|
+
name,
|
|
150
|
+
kind: normalizedKind,
|
|
151
|
+
exported: EXPLICIT_MODIFIER_PATTERN.exec(line.slice(0, typeMatch.index + typeMatch[0].length))?.[1] === "public",
|
|
152
|
+
line: lineNumber,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (scopeStack.length > 0 && depth === scopeStack[scopeStack.length - 1].bodyDepth) {
|
|
156
|
+
const currentScope = scopeStack[scopeStack.length - 1];
|
|
157
|
+
const methodMatch = line.match(METHOD_LINE_PATTERN);
|
|
158
|
+
if (methodMatch) {
|
|
159
|
+
pushMethodDeclaration(declarations, methodMatch[1], line, lineNumber);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const ctorMatch = line.match(CONSTRUCTOR_LINE_PATTERN);
|
|
163
|
+
if (ctorMatch && ctorMatch[1] === currentScope.name) {
|
|
164
|
+
pushMethodDeclaration(declarations, ctorMatch[1], line, lineNumber);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const depthBefore = depth;
|
|
169
|
+
depth += countChar(line, "{") - countChar(line, "}");
|
|
170
|
+
if (typeMatch && normalizedKind) {
|
|
171
|
+
// Always push assuming depth+1 is the body -- whether the opening
|
|
172
|
+
// brace was on this same line (K&R style, the common case) or a
|
|
173
|
+
// subsequent line (Allman style), the actual `{` character
|
|
174
|
+
// eventually seen will bump `depth` to exactly `depthBefore + 1`,
|
|
175
|
+
// matching this assumption either way.
|
|
176
|
+
scopeStack.push({ kind: normalizedKind, name: typeMatch[2], bodyDepth: depthBefore + 1 });
|
|
177
|
+
}
|
|
178
|
+
while (scopeStack.length > 0 && depth < scopeStack[scopeStack.length - 1].bodyDepth) {
|
|
179
|
+
scopeStack.pop();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (hadUnterminatedBlockComment) {
|
|
183
|
+
diagnostics.push({
|
|
184
|
+
severity: "error",
|
|
185
|
+
code: "unterminated-block-comment",
|
|
186
|
+
message: "File appears to contain an unterminated /* block comment; facts from this best-effort scan may be incomplete.",
|
|
187
|
+
path: relativePath,
|
|
188
|
+
analyzerId: JAVA_ANALYZER_ID,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const parseStatus = hadUnterminatedBlockComment ? "parse-error" : "parsed";
|
|
192
|
+
return {
|
|
193
|
+
relativePath,
|
|
194
|
+
language,
|
|
195
|
+
role,
|
|
196
|
+
parseStatus,
|
|
197
|
+
analyzerId: JAVA_ANALYZER_ID,
|
|
198
|
+
lineCount: inventoryEntry.lineCount,
|
|
199
|
+
imports,
|
|
200
|
+
exports: [],
|
|
201
|
+
declarations,
|
|
202
|
+
references: [],
|
|
203
|
+
diagnostics,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function pushMethodDeclaration(declarations, name, line, lineNumber) {
|
|
207
|
+
const kind = "method";
|
|
208
|
+
declarations.push({
|
|
209
|
+
name,
|
|
210
|
+
kind,
|
|
211
|
+
exported: EXPLICIT_MODIFIER_PATTERN.exec(line)?.[1] === "public",
|
|
212
|
+
line: lineNumber,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function countChar(line, ch) {
|
|
216
|
+
let count = 0;
|
|
217
|
+
for (let i = 0; i < line.length; i++) {
|
|
218
|
+
if (line[i] === ch)
|
|
219
|
+
count += 1;
|
|
220
|
+
}
|
|
221
|
+
return count;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Comment/string stripping (heuristic, not a tokenizer). Strips `//` line
|
|
225
|
+
// comments and `/* ... */` block comments (which may span multiple
|
|
226
|
+
// lines), and blanks out the contents of `"..."` and `'...'` literals
|
|
227
|
+
// (escape-aware) so a brace/quote character inside a string or comment can
|
|
228
|
+
// never be misread as real code structure. Does not give Java 15+ text
|
|
229
|
+
// blocks (`"""..."""`) special multi-line handling -- see this file's
|
|
230
|
+
// header comment.
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
function buildLogicalLines(content) {
|
|
233
|
+
const rawLines = content.split(/\r\n|\r|\n/);
|
|
234
|
+
const logical = [];
|
|
235
|
+
let inBlockComment = false;
|
|
236
|
+
for (const rawLine of rawLines) {
|
|
237
|
+
logical.push(stripLine(rawLine));
|
|
238
|
+
}
|
|
239
|
+
return { logical, hadUnterminatedBlockComment: inBlockComment };
|
|
240
|
+
function stripLine(line) {
|
|
241
|
+
let result = "";
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < line.length) {
|
|
244
|
+
if (inBlockComment) {
|
|
245
|
+
const closeIdx = line.indexOf("*/", i);
|
|
246
|
+
if (closeIdx === -1)
|
|
247
|
+
return result;
|
|
248
|
+
inBlockComment = false;
|
|
249
|
+
i = closeIdx + 2;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const ch = line[i];
|
|
253
|
+
if (ch === "/" && line[i + 1] === "/")
|
|
254
|
+
return result;
|
|
255
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
256
|
+
inBlockComment = true;
|
|
257
|
+
i += 2;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (ch === '"' || ch === "'") {
|
|
261
|
+
const quote = ch;
|
|
262
|
+
let j = i + 1;
|
|
263
|
+
while (j < line.length) {
|
|
264
|
+
if (line[j] === "\\") {
|
|
265
|
+
j += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (line[j] === quote) {
|
|
269
|
+
j += 1;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
j += 1;
|
|
273
|
+
}
|
|
274
|
+
i = j;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
result += ch;
|
|
278
|
+
i += 1;
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const GRADLE_BUILD_FILE_NAMES = new Set(["build.gradle", "build.gradle.kts"]);
|
|
4
|
+
const GRADLE_SETTINGS_FILE_NAMES = new Set(["settings.gradle", "settings.gradle.kts"]);
|
|
5
|
+
const GRADLE_WRAPPER_FILE_NAMES = new Set(["gradlew", "gradlew.bat"]);
|
|
6
|
+
// Conservative, single-line extraction: `rootProject.name = "foo"` (Groovy
|
|
7
|
+
// or Kotlin DSL both use this exact call-and-assign shape). Does not handle
|
|
8
|
+
// multi-line expressions, string concatenation, or computed values.
|
|
9
|
+
const GRADLE_SETTINGS_PROJECT_NAME_PATTERN = /rootProject\.name\s*=\s*["']([^"']+)["']/;
|
|
10
|
+
// Conservative, single-line/non-nested extraction: the first top-level
|
|
11
|
+
// `<artifactId>...</artifactId>` in the file. Real pom.xml can nest an
|
|
12
|
+
// `<artifactId>` inside `<parent>` or `<dependencies>`, which this naive
|
|
13
|
+
// first-match scan cannot distinguish -- so this is intentionally treated as
|
|
14
|
+
// best-effort only, matching this module's "leave null rather than guess"
|
|
15
|
+
// policy for anything structurally ambiguous. A `<parent>` block, if present
|
|
16
|
+
// before the project's own `<artifactId>`, is skipped over so the far more
|
|
17
|
+
// common case (parent declared first, then the project's own coordinates)
|
|
18
|
+
// resolves to the right value instead of the parent's.
|
|
19
|
+
const POM_ARTIFACT_ID_PATTERN = /<artifactId>\s*([^<\s][^<]*?)\s*<\/artifactId>/;
|
|
20
|
+
function findSourceSetDirectories(inventory, segment, language) {
|
|
21
|
+
const dirName = `src/${segment}/${language}`;
|
|
22
|
+
const prefix = `${dirName}/`;
|
|
23
|
+
const found = inventory.files.some((f) => f.relativePath === dirName || f.relativePath.startsWith(prefix));
|
|
24
|
+
return found ? [dirName] : [];
|
|
25
|
+
}
|
|
26
|
+
export function collectJvmProjectMetadata(targetRoot, inventory) {
|
|
27
|
+
const warnings = [];
|
|
28
|
+
const knownPaths = new Set(inventory.files.map((f) => f.relativePath));
|
|
29
|
+
const recognizedBuildFiles = inventory.files
|
|
30
|
+
.map((f) => f.relativePath)
|
|
31
|
+
.filter((relativePath) => {
|
|
32
|
+
const basename = path.posix.basename(relativePath);
|
|
33
|
+
return (GRADLE_BUILD_FILE_NAMES.has(basename) ||
|
|
34
|
+
GRADLE_SETTINGS_FILE_NAMES.has(basename) ||
|
|
35
|
+
GRADLE_WRAPPER_FILE_NAMES.has(basename) ||
|
|
36
|
+
basename === "pom.xml" ||
|
|
37
|
+
basename === "gradle.properties");
|
|
38
|
+
})
|
|
39
|
+
.sort((a, b) => a.localeCompare(b));
|
|
40
|
+
const hasGradleBuild = recognizedBuildFiles.some((p) => GRADLE_BUILD_FILE_NAMES.has(path.posix.basename(p)));
|
|
41
|
+
const hasGradleSettings = recognizedBuildFiles.some((p) => GRADLE_SETTINGS_FILE_NAMES.has(path.posix.basename(p)));
|
|
42
|
+
const hasMavenPom = knownPaths.has("pom.xml");
|
|
43
|
+
const hasGradleWrapper = recognizedBuildFiles.some((p) => GRADLE_WRAPPER_FILE_NAMES.has(path.posix.basename(p)));
|
|
44
|
+
const javaSourceDirs = findSourceSetDirectories(inventory, "main", "java");
|
|
45
|
+
const kotlinSourceDirs = findSourceSetDirectories(inventory, "main", "kotlin");
|
|
46
|
+
const javaTestDirs = findSourceSetDirectories(inventory, "test", "java");
|
|
47
|
+
const kotlinTestDirs = findSourceSetDirectories(inventory, "test", "kotlin");
|
|
48
|
+
// Source-set directory presence is a conservative signal only -- a
|
|
49
|
+
// project can validly keep Java/Kotlin files outside the conventional
|
|
50
|
+
// src/main|test/{java,kotlin} layout, so this does not attempt to prove
|
|
51
|
+
// "has no Java/Kotlin source"; it only reports the conventional-layout
|
|
52
|
+
// signal explicitly. hasJavaSource/hasKotlinSource below fall back to any
|
|
53
|
+
// scanned file of that language so they stay meaningful even for
|
|
54
|
+
// non-conventional layouts.
|
|
55
|
+
const hasJavaSource = javaSourceDirs.length > 0 || inventory.filesByLanguage.java > 0;
|
|
56
|
+
const hasKotlinSource = kotlinSourceDirs.length > 0 || inventory.filesByLanguage.kotlin > 0;
|
|
57
|
+
const hasJavaTests = javaTestDirs.length > 0;
|
|
58
|
+
const hasKotlinTests = kotlinTestDirs.length > 0;
|
|
59
|
+
const recognizedSourceDirectories = [...new Set([...javaSourceDirs, ...kotlinSourceDirs])].sort((a, b) => a.localeCompare(b));
|
|
60
|
+
const recognizedTestDirectories = [...new Set([...javaTestDirs, ...kotlinTestDirs])].sort((a, b) => a.localeCompare(b));
|
|
61
|
+
let projectName = null;
|
|
62
|
+
if (hasGradleSettings) {
|
|
63
|
+
const settingsPath = recognizedBuildFiles.find((p) => GRADLE_SETTINGS_FILE_NAMES.has(path.posix.basename(p)));
|
|
64
|
+
if (settingsPath) {
|
|
65
|
+
const content = readSafely(targetRoot, settingsPath, warnings);
|
|
66
|
+
if (content !== null) {
|
|
67
|
+
const match = content.match(GRADLE_SETTINGS_PROJECT_NAME_PATTERN);
|
|
68
|
+
if (match)
|
|
69
|
+
projectName = match[1];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (projectName === null && hasMavenPom) {
|
|
74
|
+
const content = readSafely(targetRoot, "pom.xml", warnings);
|
|
75
|
+
if (content !== null) {
|
|
76
|
+
const withoutParentBlock = content.replace(/<parent>[\s\S]*?<\/parent>/, "");
|
|
77
|
+
const match = withoutParentBlock.match(POM_ARTIFACT_ID_PATTERN);
|
|
78
|
+
if (match)
|
|
79
|
+
projectName = match[1];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
hasGradleBuild,
|
|
84
|
+
hasGradleSettings,
|
|
85
|
+
hasMavenPom,
|
|
86
|
+
hasGradleWrapper,
|
|
87
|
+
hasJavaSource,
|
|
88
|
+
hasKotlinSource,
|
|
89
|
+
hasJavaTests,
|
|
90
|
+
hasKotlinTests,
|
|
91
|
+
recognizedBuildFiles,
|
|
92
|
+
recognizedSourceDirectories,
|
|
93
|
+
recognizedTestDirectories,
|
|
94
|
+
projectName,
|
|
95
|
+
warnings,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function readSafely(targetRoot, relativePath, warnings) {
|
|
99
|
+
try {
|
|
100
|
+
return fs.readFileSync(path.join(targetRoot, relativePath), "utf8");
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
warnings.push(`Could not read ${relativePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|