@dailephd/my-dev-kit-lab 0.3.2 → 0.3.4
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 +11 -8
- package/dist/scripts/security/validate.js +8 -18
- package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +96 -0
- package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +202 -4
- package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +19 -1
- package/dist/src/audits/codeRot/detectors/testRotDetector.js +126 -0
- package/dist/src/audits/codeRot/utils/jvmSourceFactsUtils.js +153 -0
- package/dist/src/audits/core/collectSourceFacts.js +21 -10
- 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 +9 -3
- package/dist/src/audits/core/projectInventory.js +41 -0
- package/dist/src/audits/security/securityAuditAdapter.js +15 -12
- package/dist/src/securityValidation/report/writeSecurityReportFiles.js +28 -0
- package/package.json +97 -97
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.3.3 Batch 1 -- conservative, dependency-free Kotlin source analyzer.
|
|
3
|
+
//
|
|
4
|
+
// There is no Kotlin 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. Facts are extracted with the same style of
|
|
7
|
+
// brace-depth-tracking line scan as javaAnalyzer.ts -- not a real
|
|
8
|
+
// tokenizer, not an AST, and not a syntax validator.
|
|
9
|
+
//
|
|
10
|
+
// What this DOES extract:
|
|
11
|
+
// - a single `package foo.bar` declaration (no trailing `;` required)
|
|
12
|
+
// - `import foo.bar.Baz` and `import foo.bar.Baz as Alias` (recorded via
|
|
13
|
+
// the existing generic ImportFactKind "static" -- "resolved through a
|
|
14
|
+
// static, syntactic import statement", the same meaning already used
|
|
15
|
+
// by every other analyzer in this project)
|
|
16
|
+
// - class, data class, interface, and object declarations (data class ->
|
|
17
|
+
// DeclarationFactKind "class"; object -> "class", the closest safe
|
|
18
|
+
// existing kind for a singleton declaration -- there is no dedicated
|
|
19
|
+
// object kind in the shared model)
|
|
20
|
+
// - `enum class Foo` -> DeclarationFactKind "enum"
|
|
21
|
+
// - top-level function declarations (`fun foo(...)`) -> kind "function"
|
|
22
|
+
// - member function declarations directly inside a class/interface/
|
|
23
|
+
// object body -> kind "method"
|
|
24
|
+
// - visibility (public/internal/protected/private), folded into the
|
|
25
|
+
// existing `exported` boolean -- see "Visibility mapping" below
|
|
26
|
+
// - line counts, parse/analyze status, and diagnostics for degraded
|
|
27
|
+
// parsing
|
|
28
|
+
//
|
|
29
|
+
// What this explicitly does NOT do: real Kotlin compilation, type
|
|
30
|
+
// inference, extension-function semantic resolution (an extension
|
|
31
|
+
// function's receiver type is not modeled -- only the function's own
|
|
32
|
+
// identifier is recorded as its name), coroutine behavior, multiplatform
|
|
33
|
+
// source-set semantics, Android/Compose lifecycle behavior, runtime
|
|
34
|
+
// behavior, semantic duplicate-implementation proof, or dead-code proof.
|
|
35
|
+
// Property (`val`/`var`) declarations are not extracted (out of this
|
|
36
|
+
// batch's required scope, and harder to distinguish conservatively from
|
|
37
|
+
// local variable declarations via regex alone).
|
|
38
|
+
//
|
|
39
|
+
// Visibility mapping (no SourceFacts schema change):
|
|
40
|
+
// Same reasoning as javaAnalyzer.ts. Kotlin's default (no modifier) is
|
|
41
|
+
// `public`, unlike Java's package-private default -- so `exported` is
|
|
42
|
+
// true unless an explicit `internal`, `protected`, or `private` modifier
|
|
43
|
+
// is present. This intentionally collapses internal/protected/private
|
|
44
|
+
// into a single "not exported" bucket; callers needing that distinction
|
|
45
|
+
// must re-derive it from source.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export const KOTLIN_ANALYZER_ID = "kotlin-analyzer";
|
|
48
|
+
const SUPPORTED_EXTENSIONS = [".kt", ".kts"];
|
|
49
|
+
// Mirrors typescriptJavaScriptAnalyzer.ts/pythonAnalyzer.ts/javaAnalyzer.ts's
|
|
50
|
+
// bound.
|
|
51
|
+
const MAX_ANALYZABLE_BYTES = 1_000_000;
|
|
52
|
+
const PACKAGE_LINE_PATTERN = /^\s*package\s+([\w.]+)\s*;?\s*$/;
|
|
53
|
+
const IMPORT_LINE_PATTERN = /^\s*import\s+([\w.]+(?:\.\*)?)(?:\s+as\s+(\w+))?\s*;?\s*$/;
|
|
54
|
+
const VISIBILITY_WORDS = "(?:public|internal|protected|private)";
|
|
55
|
+
const OTHER_TYPE_MODIFIER_WORDS = "(?:open|final|abstract|sealed|inner|data|annotation|enum|companion|value|actual|expect)";
|
|
56
|
+
const TYPE_DECL_PATTERN = new RegExp(`^\\s*(${VISIBILITY_WORDS}\\s+)?((?:${OTHER_TYPE_MODIFIER_WORDS}\\s+)*)(class|interface|object)\\s+(\\w+)`);
|
|
57
|
+
// Optional generic receiver/type parameters and an optional extension-
|
|
58
|
+
// function receiver (`ReceiverType.`) before the function name -- the
|
|
59
|
+
// receiver itself is discarded, only the final identifier is kept as the
|
|
60
|
+
// function's name (see header comment).
|
|
61
|
+
const FUN_LINE_PATTERN = new RegExp(`^\\s*(${VISIBILITY_WORDS}\\s+)?(?:(?:override|open|final|abstract|inline|suspend|operator|infix|tailrec|external)\\s+)*fun(?:\\s*<[^>]*>)?\\s+(?:[\\w<>?.]+\\.)?(\\w+)\\s*\\(`);
|
|
62
|
+
export const KOTLIN_ANALYZER = {
|
|
63
|
+
id: KOTLIN_ANALYZER_ID,
|
|
64
|
+
supportedLanguages: ["kotlin"],
|
|
65
|
+
supportedExtensions: SUPPORTED_EXTENSIONS,
|
|
66
|
+
analyzeFile,
|
|
67
|
+
};
|
|
68
|
+
function analyzeFile(input) {
|
|
69
|
+
const { relativePath, language, role, content, inventoryEntry } = input;
|
|
70
|
+
if (Buffer.byteLength(content, "utf8") > MAX_ANALYZABLE_BYTES) {
|
|
71
|
+
return {
|
|
72
|
+
relativePath,
|
|
73
|
+
language,
|
|
74
|
+
role,
|
|
75
|
+
parseStatus: "file-level-only",
|
|
76
|
+
analyzerId: KOTLIN_ANALYZER_ID,
|
|
77
|
+
lineCount: inventoryEntry.lineCount,
|
|
78
|
+
imports: [],
|
|
79
|
+
exports: [],
|
|
80
|
+
declarations: [],
|
|
81
|
+
references: [],
|
|
82
|
+
diagnostics: [
|
|
83
|
+
{
|
|
84
|
+
severity: "info",
|
|
85
|
+
code: "file-too-large-for-analysis",
|
|
86
|
+
message: `File exceeds the ${MAX_ANALYZABLE_BYTES}-byte analyzer bound; only inventory-level facts are available.`,
|
|
87
|
+
path: relativePath,
|
|
88
|
+
analyzerId: KOTLIN_ANALYZER_ID,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const diagnostics = [];
|
|
94
|
+
const { logical, hadUnterminatedBlockComment, hadUnterminatedRawString } = buildLogicalLines(content);
|
|
95
|
+
const imports = [];
|
|
96
|
+
const declarations = [];
|
|
97
|
+
const scopeStack = [];
|
|
98
|
+
let depth = 0;
|
|
99
|
+
for (let i = 0; i < logical.length; i++) {
|
|
100
|
+
const line = logical[i];
|
|
101
|
+
const lineNumber = i + 1;
|
|
102
|
+
if (line.trim().length === 0) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const importMatch = line.match(IMPORT_LINE_PATTERN);
|
|
106
|
+
if (importMatch) {
|
|
107
|
+
imports.push({
|
|
108
|
+
source: importMatch[1],
|
|
109
|
+
kind: "static",
|
|
110
|
+
line: lineNumber,
|
|
111
|
+
...(importMatch[2] ? { importedNames: [importMatch[2]] } : {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const typeMatch = line.match(TYPE_DECL_PATTERN);
|
|
115
|
+
let isEnumClass = false;
|
|
116
|
+
if (typeMatch) {
|
|
117
|
+
isEnumClass = /(?:^|\s)enum(?:\s|$)/.test(typeMatch[2]);
|
|
118
|
+
}
|
|
119
|
+
if (typeMatch) {
|
|
120
|
+
const rawKeyword = typeMatch[3]; // "class" | "interface" | "object"
|
|
121
|
+
const name = typeMatch[4];
|
|
122
|
+
const exported = typeMatch[1] === undefined || typeMatch[1].trim() === "public";
|
|
123
|
+
declarations.push({
|
|
124
|
+
name,
|
|
125
|
+
kind: isEnumClass ? "enum" : rawKeyword === "interface" ? "interface" : "class",
|
|
126
|
+
exported,
|
|
127
|
+
line: lineNumber,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (scopeStack.length > 0 && depth === scopeStack[scopeStack.length - 1].bodyDepth) {
|
|
131
|
+
const funMatch = line.match(FUN_LINE_PATTERN);
|
|
132
|
+
if (funMatch) {
|
|
133
|
+
const exported = funMatch[1] === undefined || funMatch[1].trim() === "public";
|
|
134
|
+
declarations.push({ name: funMatch[2], kind: "method", exported, line: lineNumber });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (scopeStack.length === 0) {
|
|
138
|
+
const funMatch = line.match(FUN_LINE_PATTERN);
|
|
139
|
+
if (funMatch) {
|
|
140
|
+
const exported = funMatch[1] === undefined || funMatch[1].trim() === "public";
|
|
141
|
+
declarations.push({ name: funMatch[2], kind: "function", exported, line: lineNumber });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const depthBefore = depth;
|
|
145
|
+
depth += countChar(line, "{") - countChar(line, "}");
|
|
146
|
+
if (typeMatch) {
|
|
147
|
+
const rawKeyword = typeMatch[3];
|
|
148
|
+
// class/object and interface both get a scope frame ("object" is
|
|
149
|
+
// tracked as a "class"-kind scope for member-function purposes);
|
|
150
|
+
// whether the opening `{` lands on this line or a later one, the
|
|
151
|
+
// eventual `{` bumps depth to exactly depthBefore + 1 either way --
|
|
152
|
+
// same reasoning as javaAnalyzer.ts's scope push.
|
|
153
|
+
const scopeKind = rawKeyword === "interface" ? "interface" : "class";
|
|
154
|
+
scopeStack.push({ kind: scopeKind, bodyDepth: depthBefore + 1 });
|
|
155
|
+
}
|
|
156
|
+
while (scopeStack.length > 0 && depth < scopeStack[scopeStack.length - 1].bodyDepth) {
|
|
157
|
+
scopeStack.pop();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (hadUnterminatedRawString) {
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
severity: "error",
|
|
163
|
+
code: "unterminated-raw-string",
|
|
164
|
+
message: 'File appears to contain an unterminated """ raw string; facts from this best-effort scan may be incomplete.',
|
|
165
|
+
path: relativePath,
|
|
166
|
+
analyzerId: KOTLIN_ANALYZER_ID,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (hadUnterminatedBlockComment) {
|
|
170
|
+
diagnostics.push({
|
|
171
|
+
severity: "error",
|
|
172
|
+
code: "unterminated-block-comment",
|
|
173
|
+
message: "File appears to contain an unterminated /* block comment; facts from this best-effort scan may be incomplete.",
|
|
174
|
+
path: relativePath,
|
|
175
|
+
analyzerId: KOTLIN_ANALYZER_ID,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const parseStatus = hadUnterminatedRawString || hadUnterminatedBlockComment ? "parse-error" : "parsed";
|
|
179
|
+
return {
|
|
180
|
+
relativePath,
|
|
181
|
+
language,
|
|
182
|
+
role,
|
|
183
|
+
parseStatus,
|
|
184
|
+
analyzerId: KOTLIN_ANALYZER_ID,
|
|
185
|
+
lineCount: inventoryEntry.lineCount,
|
|
186
|
+
imports,
|
|
187
|
+
exports: [],
|
|
188
|
+
declarations,
|
|
189
|
+
references: [],
|
|
190
|
+
diagnostics,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function countChar(line, ch) {
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (let i = 0; i < line.length; i++) {
|
|
196
|
+
if (line[i] === ch)
|
|
197
|
+
count += 1;
|
|
198
|
+
}
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Comment/string stripping (heuristic, not a tokenizer). Strips `//` line
|
|
203
|
+
// comments and `/* ... */` block comments (which may span multiple lines),
|
|
204
|
+
// blanks out `"..."`/`'...'` single-line literal contents (escape-aware),
|
|
205
|
+
// and blanks out `"""..."""` raw strings (which may span multiple lines,
|
|
206
|
+
// no escape processing inside -- mirrors pythonAnalyzer.ts's triple-quote
|
|
207
|
+
// handling, since Kotlin raw strings share the same "arbitrary content
|
|
208
|
+
// until the next matching triple-quote marker" shape).
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
function buildLogicalLines(content) {
|
|
211
|
+
const rawLines = content.split(/\r\n|\r|\n/);
|
|
212
|
+
const logical = [];
|
|
213
|
+
let inBlockComment = false;
|
|
214
|
+
let inRawString = false;
|
|
215
|
+
for (const rawLine of rawLines) {
|
|
216
|
+
if (inRawString) {
|
|
217
|
+
const closeIdx = rawLine.indexOf('"""');
|
|
218
|
+
if (closeIdx === -1) {
|
|
219
|
+
logical.push("");
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
inRawString = false;
|
|
223
|
+
logical.push(stripLine(rawLine.slice(closeIdx + 3)));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
logical.push(stripLine(rawLine));
|
|
227
|
+
}
|
|
228
|
+
return { logical, hadUnterminatedBlockComment: inBlockComment, hadUnterminatedRawString: inRawString };
|
|
229
|
+
function stripLine(line) {
|
|
230
|
+
let result = "";
|
|
231
|
+
let i = 0;
|
|
232
|
+
while (i < line.length) {
|
|
233
|
+
if (inBlockComment) {
|
|
234
|
+
const closeIdx = line.indexOf("*/", i);
|
|
235
|
+
if (closeIdx === -1)
|
|
236
|
+
return result;
|
|
237
|
+
inBlockComment = false;
|
|
238
|
+
i = closeIdx + 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const ch = line[i];
|
|
242
|
+
if (ch === "/" && line[i + 1] === "/")
|
|
243
|
+
return result;
|
|
244
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
245
|
+
inBlockComment = true;
|
|
246
|
+
i += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (line.slice(i, i + 3) === '"""') {
|
|
250
|
+
const closeIdx = line.indexOf('"""', i + 3);
|
|
251
|
+
if (closeIdx === -1) {
|
|
252
|
+
inRawString = true;
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
i = closeIdx + 3;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (ch === '"' || ch === "'") {
|
|
259
|
+
const quote = ch;
|
|
260
|
+
let j = i + 1;
|
|
261
|
+
while (j < line.length) {
|
|
262
|
+
if (line[j] === "\\") {
|
|
263
|
+
j += 2;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (line[j] === quote) {
|
|
267
|
+
j += 1;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
j += 1;
|
|
271
|
+
}
|
|
272
|
+
i = j;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
result += ch;
|
|
276
|
+
i += 1;
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
}
|