@dailephd/my-dev-kit-lab 0.3.2 → 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.
@@ -1,10 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { collectPythonProjectMetadata } from "../../core/pythonProjectMetadata.js";
4
+ import { collectJvmProjectMetadata } from "../../core/jvmProjectMetadata.js";
4
5
  import { readBoundedFileText } from "../utils/boundedRead.js";
6
+ import { baseNameNoExt } from "../utils/filePatternUtils.js";
5
7
  import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
6
8
  import { getParsedSourceFacts, indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
7
9
  import { splitLines } from "../utils/textLines.js";
10
+ import { jvmImportCandidatePaths, jvmImportLooksPossiblyLocal, jvmImportSimpleName, } from "../utils/jvmSourceFactsUtils.js";
8
11
  // ---------------------------------------------------------------------------
9
12
  // v0.3.0 Batch 4 — test-rot detector.
10
13
  //
@@ -51,6 +54,9 @@ export const TEST_ROT_DETECTOR = {
51
54
  // required condition -- its absence must not change whether a finding
52
55
  // is emitted, only add or omit one extra evidence line.
53
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);
54
60
  for (const testFile of ctx.inventory.testFiles) {
55
61
  const content = readBoundedFileText(ctx.target.rootPath, testFile.relativePath, testFile.sizeBytes, MAX_READ_BYTES);
56
62
  if (content === null)
@@ -87,6 +93,15 @@ export const TEST_ROT_DETECTOR = {
87
93
  // this guard is the fix for that latent cross-language bug.
88
94
  issues.push(...findMissingPythonSourceImports(ctx, testFile.relativePath, parsedFacts, pythonMetadata));
89
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
+ }
90
105
  else if (parsedFacts) {
91
106
  issues.push(...findMissingSourceImportsFromSourceFacts(ctx, testFile.relativePath, parsedFacts));
92
107
  }
@@ -291,6 +306,117 @@ function findMissingPythonSourceImports(ctx, testRelativePath, facts, pythonMeta
291
306
  }
292
307
  return issues;
293
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
+ }
294
420
  function findStaleNpmRunReferences(testRelativePath, content, scriptNames) {
295
421
  const issues = [];
296
422
  const seen = new Set();
@@ -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
+ }
@@ -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
+ }