Java-Direct: Iteration Results Log

Current status: :compiler:java-direct:test full suite green, 2839/2839 (100%). No known won't-fix.

Last archived: implDocs/archive/ITERATION_RESULTS_2026_07_13.md (entries through 2026-07-13).


How to write entries

This log is read into the agent's context every session, so entries must stay short.

  • Newest entry on top. One entry per landed change or per investigated regression.
  • Cap each entry at ~15 lines / ~150 words. If the rationale, a trace, or a measurement table is longer, put it in a dedicated implDocs/<TOPIC>.md and link to it from the entry — do not inline it here.
  • Use the fixed fields below. No free-form multi-paragraph narration; if a field needs more than ~2 lines, link out instead.
  • No pasted logs, stacktraces, or diffs. Quote the single line that matters; link the rest.
  • Archive when this file passes ~600 lines (see AGENT_INSTRUCTIONS.md → “Docs Maintenance”): git mv it to implDocs/archive/ITERATION_RESULTS_<last-entry-date>.md, add an archive banner, and reset this file to the template below.

Entry template

### YYYY-MM-DD — <one-line title>
- **Change**: what changed and why (1–3 lines).
- **Files**: key files touched (+N/−M LoC if useful).
- **Tests**: suites run + counts (e.g. box 1178/1178, phased 1513/1513).
- **Result**: green / regression fixed / won't-fix — link to a detail doc if there is one.

2026-07-31 — Unit tests use JUnit asserters; lightweight scan of malformed package names

  • Change: reviewer follow-up on JavaParsingLightweightScannerTest. Every raw Kotlin assert(...) in the module's unit tests (a no-op without -ea) is now a JUnit assertion — assertEquals/assertTrue/assertFalse/assertNull/assertSame and the contract-carrying org.junit.jupiter.api.assertNotNull, which let the following !! go. Messages that only restated expected/actual are dropped. New scanner tests: a package name split across a line and a block comment (package builder // c \n . /* c */ subpackage;), package com.123;, and class 456 {} with and without a well-formed sibling. extractFileInfoLightweight now joins identifier segments instead of appending identifiers and dots verbatim, so a malformed name degrades to its valid prefix (com) rather than the stray-dot com..
  • Files: util/JavaSourceIndex.kt (+3/−3); all 10 unit test files under test/ (~440 assertions rewritten), JavaParsingLightweightScannerTest.kt +4 tests.
  • Tests: unit tests 121/121 green (12 classes); box + phased green (2795 executed, 0 failures, 0 errors).
  • Result: green.

2026-07-31 — Lazy enum-entry annotations without the fragile mutable list

  • Change: the previous schema let FirLazyJavaAnnotationMutableList sit in FirEnumEntryImpl's MutableOrEmptyList slot, and depended on nothing but isEmpty being called before the list materialised. Instead FirEnumEntryImpl.annotations is now a plain var annotations: List<FirAnnotation> whose replace installs the given list as is, so FirJavaFacade can install a FirLazyJavaAnnotationList (same class the members use) via replaceAnnotations after buildEnumEntry. The only cost is transformAnnotations, which maps into a new list instead of transforming in place.
  • Files: fir-jvm/.../FirJavaFacade.kt, fir-jvm/.../enhancement/FirJavaAnnotationList.kt (−29, FirLazyJavaAnnotationMutableList deleted); FIR tree generator: new ListField.isAssignableList + useAssignableList("annotations") on impl(enumEntry); FirEnumEntryImpl/FirEnumEntryBuilder regenerated. The builder-side var-list support in generators/tree-generator-common is reverted — those files are identical to master again.
  • Tests: box + phased green (2795 executed, 0 failures, 0 errors); JavaParsingTest and JavaCycleBreakerTest green; both regression gates green (PhasedJvmDiagnosticLightTreeTestGenerated.*, *CompileKotlinAgainstKotlin*).
  • Result: green; enum entries and members now share one lazy annotation list class.

2026-07-31 — Delete JavaSupertypeGraph: the duplicated supertype-reference resolver was test-only

  • Change: answers the reviewer‘s standing objection to resolveSupertypeReference (“we partly repeat type resolution logic here”). JavaSupertypeGraph re-derived import lookup, star-import candidates and package/class splits from the AST, and returned an empty list for any dotted supertype — an unspecified under-approximation. Repo-wide, its two entry points (LeanJavaClassFinder.getDirectSupertypes / collectInheritedInnerClasses) had no production callers: the test fake in JavaParsingTestBase implemented both as empty, and only JavaParsingClassFinderTest invoked the real ones. Production already resolves the first supertype level through the class’s own JavaResolutionContext (resolveInheritedInnerClassToClassId) and deeper levels through directSupertypeClassIds.
  • Files: util/JavaSupertypeGraph.kt (−270, deleted), JavaClassFinderOverAstImpl.kt, resolution/LeanJavaClassFinder.kt, resolution/JavaModelSessionAccess.kt (KDoc), implDocs/ARCHITECTURE.md, test/JavaParsingTestBase.kt, test/JavaParsingClassFinderTest.kt.
  • Test rework: JavaParsingClassFinderTest re-pointed at the real path — the B<String>.C case is now a positive assertion (the inherited a.B.C.N must resolve; it previously only asserted the wrongly-truncated a.B was absent), plus a new same-level multi-supertype test covering the single-match and ambiguous-decline arms of walkSupertypeClassIds. Two graph-only tests (testGetDirectSupertypesUsesCache, testDiamondInheritanceInnerClasses) were dropped: they asserted the deleted cache, and the transitive/grandparent case they gestured at is already covered end-to-end by qualifiedInheritedNestedClassInOwnImplementsClause.kt.
  • Tests: JavaParsing* unit tests green (JavaParsingClassFinderTest 12/12); full box + phased green (2795 executed, 0 failures, 0 errors).
  • Result: green; per-representation duplicate removed, one supertype walk remains.

2026-07-31 — Box guards for the JLS accessibility check, incl. a genuinely binary Java supertype

  • Change: complements the phased guard below with two box tests. Unlike diagnostics tests (which hard-code DependencyKind.Source), a codegen // MODULE: lib dependency is really compiled: JavaCompilerFacade.compileJavaFiles javac-compiles lib's .java into lib's output dir, and JvmEnvironmentConfigurator.registerModuleDependencies puts that dir on main's classpath — so main reads a.Base as a binary Java class through FirBackedJavaClassAdapter, i.e. the exact shape of the IJ-FP testIntellij_exceptionAnalyzer regression. So a box test can express what a phased test cannot.
  • Files: testData/codegen/box/javaDirect/packagePrivateInheritedNestedClassNotVisibleAcrossPackages.kt (source arm, single module) and testData/codegen/boxJvm/javaDirect/packagePrivateInheritedNestedClassFromBinaryModule.kt (adapter arm, libmain) — both new; no generator change needed.
  • Tests: full JavaUsingAstPhasedTestGenerated + JavaUsingAstBoxTestGenerated green (2795 executed, 0 failures, 0 errors); shared-data gate green in Fir{LightTree,Psi}BlackBoxCodegenTestGenerated$Box{,Jvm}$JavaDirect.
  • Result: green; non-vacuity verified with two probes — forcing FirBackedJavaClassAdapter.visibility to Visibilities.Public fails only the binary-module test, while short-circuiting isInheritedNestedClassAccessible to true fails both. Both probes reverted (git diff on src empty). Box data needs no golden file, so rule 5/6 concerns about shared diagnostic data do not apply.

2026-07-31 — Phased guard for the adapter's JLS accessibility check (via // MODULE)

  • Change: the FirBackedJavaClassAdapter.visibility arm of isInheritedNestedClassAccessible (2026-07-14, IJ-FP testIntellij_exceptionAnalyzer) had no small-test guard: the adapter is only consulted for classes outside the module‘s own Java source index, and a phased test always gets Java as sources. // MODULE: lib / // MODULE: main(lib) closes that gap — a dependency module’s Java class is not in main's JavaSourceRoot set (JavaDirectFacadeBuilder.kt:40), so classifierAdapterFor returns the adapter exactly as it does for binary Java.
  • Files: testData/diagnostics/tests/jvm/javaDirect/packagePrivateInheritedNestedClassFromOtherModule.kt (new).
  • Tests: new test green in JavaUsingAstPhasedTestGenerated and in both PSI gates (PhasedJvmDiagnostic{LightTree,Psi}TestGenerated); full JavaUsingAstPhasedTestGenerated re-run green, 0 FAILED.
  • Result: green; non-vacuity verified — forcing visibility back to Visibilities.Public makes it fail (“Actual data differs”). Note: diagnostics tests hard-code DependencyKind.Source, so this is not a compiled-jar dependency; a genuinely binary Java dependency still needs PROVIDE_JAVA_AS_BINARIES (foreign-annotations roots only) or a box test.

2026-07-30 — Binary index remeasured on KotlinFullPipelineTestsGenerated; not the M1 0.5%

  • Change: JavaClassFinderOverBinaryIndex no longer builds an FqName per lookup: the top-level cache is now two-level, keyed by the classId's own packageFqName + outermost class Name (new FqName.topLevelName()), so the string concatenation, pathSegments() list, FqName/ FqNameUnsafe pair and re-hash are gone from the hot path (378994 lookups, ~1.5M objects per corpus). Paired in-run A/B: 631 -> 465 ns per hit lookup (-26%), but only ~54 ms per corpus.
  • Measured (413-module FP corpus, JDK 8 launcher, 8 GB heap, M5 Max): the whole binary finder is 9.47 s = 0.99% of 958 s of compilation, of which 72% is readBinaryJavaClass on 69617 class files — work master does through the same reader — 18% knownClassNamesInPackage (2.05M name strings, 35306 traversals), 5% top-level index traversals. Allocation/GC with -Xjava-direct on vs off is identical (216-221 GB per corpus, 112-119 young GCs, 8-9 full GCs), so extra garbage is not the mechanism. The regression does not reproduce here: two A/B batches disagree on the sign (±4%), i.e. developer-machine wall time cannot resolve 0.5%.
  • Files: JavaClassFinderOverBinaryIndex.kt, review doc §12 (method, tables, and seven ranked strategies for locating the gap on the mini), this file. All harnesses removed.
  • Tests: java-direct box+phased green; full KotlinFullPipelineTestsGenerated (413 modules) green.
  • Result: index exonerated as the carrier of the M1 delta; see §12.5 for next steps.

2026-07-30 — TopLevelClassFiles shape: measured, holder kept

  • Change: comment-only. Benchmarked the merged cache's value shape (holder object vs two-element array vs SmartList-style encoded slot vs the pre-merge two maps) on the real workload from §10 (1469/1615 finders, 19k/12k cached keys, 77k/42k lookups per suite). The whole cache costs <1 ms and ~1.4 MB per full suite; the spread between shapes is ~0.15 ms/suite (~0.1 µs per compilation). The array is not faster — it is the slowest single-map variant on a 20× corpus (26.7 ms vs 23.4 ms) and loses the type/names; the encoded slot saves 33% of the allocation but trades the scoped/unscoped distinction for an unchecked encoding. Kept the named holder, added a KDoc pointer to implDocs/BINARY_SOURCE_DIVIDE_REVIEW_2026_07_22.md §11.
  • Files: JavaClassFinderOverBinaryIndex.kt (KDoc +5), review doc §11, this file.
  • Tests: java-direct box+phased green (no behaviour change).
  • Result: won't-fix by measurement — see §11.

2026-07-30 — One top-level-class cache in the binary finder; roll back two no-op shared-file diffs

  • Change: (1) JavaClassFinderOverBinaryIndex had two FqName -> VirtualFile? maps differing only in firstOrNull { it in scope } vs firstOrNull(). Measured over both suites (3084 finder instances): 69–75% of scoped keys were also in the all-scope map, every shared key had an identical value, and no lookup ever saw >1 candidate or a candidate outside scope (multiCandidate=0, noCandidateInScope=0) — see implDocs/BINARY_SOURCE_DIVIDE_REVIEW_2026_07_22.md §10. Replaced by one FqName -> TopLevelClassFiles(anywhere, inScope) cache filled in a single index pass, which keeps the two answers distinct while dropping ~12k redundant index lookups per suite (−39%). (2) Reverted FirJvmConflictsChecker + the FirSession.getJavaClassLikeSymbolByClassId forwarder and FirDirectJavaActualDeclarationExtractor to master: master already bypassed the composite provider, so both diffs were pure restyling, and no fixture covers the Kotlin-vs-binary-Java redeclaration case the checker's comment described.
  • Files: JavaClassFinderOverBinaryIndex.kt (−12/+15); reverted FirJvmConflictsChecker.kt, fir-jvm/JavaSymbolProvider.kt, FirDirectJavaActualDeclarationExtractor.kt (shared-file diff vs master for these three is now zero).
  • Tests: java-direct box+phased green, PhasedJvmDiagnosticLightTreeTestGenerated green, FirLightTreeBlackBoxCodegenTestGenerated*CompileKotlinAgainstKotlin* green.
  • Result: green.

2026-07-29 — Fail-safe finder dispatch: identify the source scope, default to binary

  • Change: createJavaDirectJavaFacadeBuilder now takes javaSourcesScope instead of librariesScope and dispatches scope === javaSourcesScope -> JavaClassFinderOverAstImpl, every other scope -> JavaClassFinderOverBinaryIndex over that scope. Previously the single whitelisted case was the library scope, so any new binary scope (IC output, HMPP-fragment classpath) would silently have been answered by the source AST finder. Memo map is now an IdentityHashMap keyed by the scope object (was System.identityHashCode + ct.sym flag in a data class — collision-prone with several scopes in play); the CliVirtualFileFinderFactory lookup is hoisted out of the lambda. Also routed the HMPP-common library session (JvmFrontendPipelinePhase.kt:397) through javaDirectFacade: it already asks for context.librariesScope, so it reuses the same memoized binary finder, and it is a literal no-op when useJavaDirect is off. The IC precompiled-binaries provider still constructs the PSI facade deliberately — it has no test coverage (see the 2026-07-29 review notes).
  • Files: JavaDirectFacadeBuilder.kt (−13/+16), JvmFrontendPipelinePhase.kt (2 lines), implDocs/ARCHITECTURE.md §3.
  • Tests: java-direct box+phased 2792/2792, PhasedJvmDiagnosticLightTreeTestGenerated 10988/10991 (3 pre-existing skips).
  • Result: green. The green java-direct suite is now itself evidence the source branch is taken: if the scope identity failed to match, the source session would receive the binary finder and every Java-source test would fail.

2026-07-29 — Express binary/source sidedness by wiring, not by two lookup vocabularies

  • Change: dropped both parallel APIs added by the divide commit. (1) Removed the source-only probes isInSourceIndex / hasPackageInSources / sourceClassNamesInPackage from JavaClassFinder + FirJavaFacade; every production finder is already single-sided by construction, so JavaSymbolProvider is back on hasTopLevelClassOf / hasPackage / knownClassNamesInPackage (this restores a real AST-index gate on java-direct, where isInSourceIndex was a constant true, and re-enables the facade caches). (2) Deleted the JvmBinaryClassFinderInputs seam: JvmBinaryClassFinderInputsOverIndex became JavaClassFinderOverBinaryIndex : JavaClassFinder, absorbing LibraryJavaClassFinder / BinaryPackageInfoJavaPackage, so JvmClassFileBasedSymbolProvider has one input (javaFacade), no ?: fallbacks and no flag awareness. The packagePartProvider fallback in hasPackage (for @file:JvmPackageName) is now unconditional.
  • Files: JavaClassFinder.kt, FirJavaFacade.kt, JavaSymbolProvider.kt, JvmClassFileBasedSymbolProvider.kt, FirJvmSessionFactory.kt, JvmFrontendPipelinePhase.kt, JavaDirectFacadeBuilder.kt, +JavaClassFinderOverBinaryIndex.kt, −JvmBinaryClassFinderInputs.kt, −JvmBinaryClassFinderInputsOverIndex.kt.
  • Tests: java-direct box+phased 2792/2792, PhasedJvmDiagnosticLightTreeTestGenerated 10991/10991, FirLightTreeBlackBoxCodegenTestGenerated (full) 10643/10643 incl. CompileKotlinAgainstKotlin.
  • Result: green. See the superseded banner in implDocs/BINARY_SOURCE_DIVIDE_REVIEW_2026_07_22.md.

2026-07-29 — Re-derive the KT-74097 guard rationale after enum-entry annotations went lazy

  • Change: re-traced whether the lazy Java annotation lists retire any cycle breaker. They do not: they removed the only known crashing trigger (@Deprecated enum constant, testIntellij_vcs_git), demoting cycleSafeClassLikeSymbol to genuine defense-in-depth, but the cycle class stays reachable — the declarations lazy reads FirJavaClass.typeParameters, whose bound enhancement iterates the class's own annotations via extractDefaultQualifiers (plus a raw outer-class getClassLikeSymbolByClassId); the enum-entry returnTypeRef is still eager because SignatureEnhancement requires a resolved ref; and 3 of 5 guard call sites carry no annotation (const-field values, @Target, type-argument substitution). The other breakers (cycleGuardedSupertypeWalk, supertype memoization, local visited sets) are annotation-agnostic. Docs/comments updated accordingly; also fixed the stale JavaSupertypeLoopChecker name (the code is cycleGuardedSupertypeWalk / JavaModelSupertypeWalkGuard).
  • Files: resolution/JavaModelSessionAccess.kt, test/.../JavaCycleBreakerTest.kt (comments only), AGENT_INSTRUCTIONS.md.
  • Tests: not run — comment/docs-only, verified via git diff that no code line changed.
  • Result: green (no code changes).

2026-07-29 — Clamp the lightweight scanner's brace/paren balance at zero

  • Change: review of extractFileInfoLightweight against its production ancestor SingleJavaFileRootsIndex.JavaSourceClassIdReader (cli-base). Unmatched closers drove the balances negative, so atTypeDeclaration() (== 0) stopped firing and every top-level type after a stray }/) was lost — for a stray } before the file's namesake class the file is dropped from the index entirely (tryBuildFileEntry requires the base name). Now clamped, mirroring Kotlin.flex's if (lBraceCount == 0) popState() else lBraceCount--; no-op on well-formed input. Also dropped a dangling KDoc, the redundant if (at(SEMICOLON)) advance() (the class-scan loop skips it anyway) and mapped an empty package name to null. Divergences from the ancestor are intentional and stay: the break@loop on a non-package token (upstream appends the first class name to the package on a missing ;), no isPackageInfo arm (JavaPackageIndexer routes package-info.java to JavaPackageInfoIndexer first), and when (lexer.getTokenType()) instead of four at() calls. getTokenType() itself is a cached field read (JavaLexer.locateToken: if (myTokenType != null) return), so repeated calls need no hoisting; getTokenText() is the allocating one, and it is only reached at balance 0.
  • Files: util/JavaSourceIndex.kt (+7/−12); JavaParsingLightweightScannerTest.kt (+47, 2 tests).
  • Tests: JavaParsing* 16/16; box+phased 2792 executed / 0 FAILED. Both new tests fail without the clamp (got [Broken], got [Foo]).
  • Result: green (error-tolerance fix).

2026-07-29 — Delete the JavaSourceFileReader abstraction; read via File.readText

  • Change: after the VirtualFileFile switch the interface had a single implementation (DefaultJavaSourceFileReader), walkSourceRoots had no callers (it served the deleted eager buildIndex), and no production or test call site ever substituted a reader — the parameter was threaded through four collaborators for nothing. Replaced by one internal readJavaSourceFileText(File): String?; the reader parameter is gone from JavaClassCache, JavaPackageIndexer, JavaPackageInfoIndexer, JavaSupertypeGraph, JavaClassFinderOverAstImpl and extractFileInfoLightweight. Content is now read with File.readText() instead of String(readBytes(), UTF_8): measured over 1503 repo .java files (37 MB, interleaved rounds, 7 samples) the medians are 46 ms vs 48 ms — ~2 ms of pure read time per full corpus, i.e. negligible against lexing/parsing.
  • Files: deleted util/JavaSourceFileReader.kt (−57), added util/javaSourceFileText.kt (+19); JavaClassCache.kt, JavaClassFinderOverAstImpl.kt, JavaPackageIndexer.kt, JavaPackageInfoIndexer.kt, util/JavaSourceIndex.kt, util/JavaSupertypeGraph.kt; tests JavaParsingTestBase.kt, JavaParsingLightweightScannerTest.kt; implDocs/ARCHITECTURE.md.
  • Tests: JavaParsing* green; box+phased 2790 executed / 0 FAILED.
  • Result: green (behaviour-preserving simplification).

2026-07-28 — Replace FirJavaEnumEntry with buildEnumEntry + lazy MutableList annotations

  • Change: review follow-up on the 2026-07-21 KT-74097 fix — the hand-written FirJavaEnumEntry duplicated FirEnumEntryImpl (~180 LoC) just to host a lazy annotation slot. The tree generator gained a per-field opt-in (LeafBuilder.listFieldsWithVar + useVarForListField DSL in AbstractBuilderConfigurator; configured only for builder(enumEntry) in the FIR BuilderConfigurator), so default builder generation is unchanged and only FirEnumEntryBuilder.annotations becomes var. The enum-entry arm of convertJavaFieldToFir uses plain buildEnumEntry with a new FirLazyJavaAnnotationMutableList — an AbstractMutableList composing a plain FirLazyJavaAnnotationList (conversion reused via toMutableList() on first mutable access; only the 5 abstract members are overridden; cheap isEmpty keeps toMutableOrEmpty() from forcing conversion) — and FirJavaLazyDeprecationsProvider. FirJavaEnumEntry deleted.
  • Files: generators/tree-generator-common/.../Builder.kt, AbstractBuilderPrinter.kt, config/AbstractBuilderConfigurator.kt, fir/tree/tree-generator/.../BuilderConfigurator.kt, regenerated fir/tree/gen/.../builder/FirEnumEntryBuilder.kt (single-line valvar), fir-jvm/.../FirJavaFacade.kt, FirJavaAnnotationList.kt, javaAnnotationsMapping.kt; deleted FirJavaEnumEntry.kt.
  • Tests: box+phased green (0 FAILED); PSI gate + CompileKotlinAgainstKotlin gate green (shared fir-jvm + fir-tree edits).
  • Result: green (behaviour-preserving simplification; laziness retained).

2026-07-28 — Comment-style cleanup of the binary/source-divide branch

  • Change: rewrote/deleted LLM-verbose comments added since the divide commit (added comment lines ~290 → ~125): dropped restatements of obvious code, caller inventories, counterfactual “rather than” phrasing, per-@param chatter, Stage/§/implDocs references in shared fir-jvm sources; kept short why-notes, API contracts, and regression/testData guards. Codified the rules in AGENT_INSTRUCTIONS.mdSource Comment Conventions (“default is no comment” gate, ~3% density baseline).
  • Files: comment-only edits across ~20 files in cli-base, cli-jvm, fir-jvm, frontend.common.jvm, core/compiler.common.jvm, java-direct/{src,test}; docs: AGENT_INSTRUCTIONS.md (conventions rewrite, reference-table refresh, stale status header).
  • Tests: not run — comment-only change, verified via git diff that no code line changed.
  • Result: green (no code changes).

2026-07-27 — Make Java-source package directory descent case-sensitive

  • Change: JavaPackageIndexer‘s per-package directory descent used File(dir, segment).isDirectory (added 2026-07-21 when the source path moved from VirtualFile to java.io.File), which is case-insensitive on macOS/Windows. A sibling source dir (syntax/logger, platform/ml/session) was wrongly accepted as package Logger/Session, so nested-class imports like com.intellij.platform.syntax.Logger.Attachment mis-split into a package prefix and reported UNRESOLVED_IMPORT. Descent now matches against the parent’s real child names via File.list() (case-sensitive), mirroring the binary index / PSI VFS findChild.
  • Files: JavaPackageIndexer.kt (new descendDirectoriesCaseSensitive, used by findPackageDirectories + findPackageDirectoryUnder).
  • Tests: box+phased green (0 FAILED); IntelliJFullPipelineTestsGenerated.testIntellij_platform_syntax and testIntellij_platform_ml now pass with java-direct on.
  • Result: regression fixed.

2026-07-22 — Gate the binary seam on useJavaDirect; delete dead finder; dedup the ASM binary reader

  • Change: applied implDocs/BINARY_SOURCE_DIVIDE_REVIEW_2026_07_22.md §4.1/§4.2/§4.7. §4.1: the binary deserializer seam is now gated on configuration.useJavaDirect in prepareJvmSessions — ON uses JvmBinaryClassFinderInputsOverIndex, OFF returns null so the deserializer falls back to the PSI FirJavaFacade binary reader (both source and binary now share one flag). §4.2: removed dead CombinedJavaClassFinder.kt (no references). §4.7: extracted the shared readBinaryJavaClass core (caching + inner-class dispatch + ClassifierResolutionContext) into frontend.common.jvm; both JvmBinaryClassFinderInputsOverIndex and the binary branch of KotlinCliJavaFileManagerImpl delegate to it. Investigated §4.1's “turn off PSI finder creation”: not doable now — KotlinCliJavaFileManagerImpl still backs JPMS module-info resolution (ClasspathRootsResolverJavaModuleInfo.readfindClass) regardless of the flag, and its PSI class-loading branch is already inert by default (usePsiClassFilesReading=false).
  • Files: cli-jvm/.../JvmFrontendPipelinePhase.kt, cli-base/.../KotlinCliJavaFileManagerImpl.kt, frontend.common.jvm/.../classFiles/BinaryJavaClassReader.kt (new), JvmBinaryClassFinderInputsOverIndex.kt; deleted CombinedJavaClassFinder.kt.
  • Tests: box+phased green (0 FAILED); PSI (PhasedJvmDiagnosticLightTreeTestGenerated) 0 fail, CompileKotlinAgainstKotlin 0 fail (shared-pipeline + file-manager edits), KotlinCliJavaFileManagerTest 7/7.
  • Result: green (behaviour-preserving refactor + flag-gated seam).

2026-07-21 — Thread java.io.File through the Java-source indexing path (drop internal VirtualFile)

  • Change: the module no longer relies on com.intellij.openapi.vfs.VirtualFile for its own source-file representation/reading; source roots are consumed as java.io.File (the CLI's JavaSourceRoot.file is already a File, so the old VFS-resolution step is removed). Per-package directory descent uses File(dir, segment)/listFiles(); content is read via readBytes() decoded as UTF-8 (unchanged charset). Binary-class-finder/CLI wiring stays on VirtualFile intentionally (external contract).
  • Files: JavaDirectFacadeBuilder.kt, JavaPackageIndexer.kt, JavaPackageInfoIndexer.kt, util/JavaSourceFileReader.kt, util/JavaSourceIndex.kt; tests JavaParsingTestBase.kt, JavaParsingClassFinderTest.kt, JavaParsingLightweightScannerTest.kt.
  • Tests: box+phased green (0 FAILED); JavaParsing* green.
  • Result: green. Watch-point: a missing source root now drops later in the pipeline (isDirectory/isFile) rather than at VFS resolution — equivalent end behaviour.

2026-07-21 — Defer Java enum-entry annotations via FirLazyJavaAnnotationList (KT-74097)

  • Change: the enum-entry arm of convertJavaFieldToFir resolved annotations eagerly while materialising FirJavaClass.declarations, which could re-enter the same in-flight ClassId (self-cycle). New FirJavaEnumEntry (mirrors FirJavaField) backs annotations/ deprecationsProvider with FirLazyJavaAnnotationList, so no eager resolution happens while declarations is built. Removed the now-dead setAnnotationsFromJava; the cycleSafeClassLikeSymbol guard is now defense-in-depth, not the sole crash preventer.
  • Files: fir-jvm/.../declarations/FirJavaEnumEntry.kt (new), fir-jvm/.../FirJavaFacade.kt, fir-jvm/.../javaAnnotationsMapping.kt, JavaCycleBreakerTest.kt (comment).
  • Tests: box+phased green; PSI (PhasedJvmDiagnosticLightTreeTestGenerated) + CompileKotlinAgainstKotlin gates green (shared fir-jvm edit).
  • Result: green.

2026-07-21 — Reuse AST name extraction in JavaSupertypeGraph (drop splitCanonicalFqName)

  • Change: supertype-reference name splitting no longer re-implements type-resolution via the generic-bracket-aware splitCanonicalFqName text scan; it reuses the AST-based extractReferenceNameParts (extracted from JavaClassifierTypeOverAst into JavaTypeOverAst), reading JAVA_CODE_REFERENCE identifier segments directly. The generic-argument edge case (a.B<String>.C) is preserved.
  • Files: util/JavaSupertypeGraph.kt, model/JavaTypeOverAst.kt.
  • Tests: box+phased green (0 FAILED); JavaParsing* green.
  • Result: green.

2026-07-21 — Scan Java-lexer tokens in JavaSourceIndex; exclude comment/bad tokens from the light-tree root

  • Change: extractFileInfoLightweight scans the Java-lexer token stream (JavaSyntaxDefinition.createLexer) instead of regex/comment-stripping to find the package name and top-level type names, using brace/paren balance for nesting; removed PACKAGE_REGEX/DECLARATION_REGEX, manual comment stripping, and the now-unused reader openLineReader. JavaLightTree synthetic-root children now also exclude comments (root-only, each declaration keeps its DOC_COMMENT for @deprecated) and BAD_CHARACTER.
  • Files: util/JavaSourceIndex.kt, util/JavaSourceFileReader.kt, parse/JavaLightTree.kt.
  • Tests: JavaParsing* (incl. lightweight scanner) green; box+phased green (0 FAILED).
  • Result: green.

2026-07-20 — Perf review: memoize recomputed reads in the Java-source model

  • Change: the model layer recomputed pure, AST-derived values on every access. Converted the hot ones to by lazy(PUBLICATION) (same precedent as supertypes/typeParameters): class keyword flags (isInterface/isEnum/isRecord/isAnnotationType/isSealed), methods/fields/constructors/recordComponents/innerClassNames/annotations; per-member resolutionContext/valueParameters/returnType and field leadingFieldNode/modifierList/type/initializerNode/annotations; type rawTypeNameParts/typeArguments. Memoizing the class collections is the key enabler — member wrappers are now stable, so the per-member lazies actually cache. Behaviour-preserving (pure functions of the immutable AST + already-lazy classifier).
  • Files: model/JavaClassOverAst.kt, model/JavaMemberOverAst.kt, model/JavaTypeOverAst.kt.
  • Tests: box+phased 2767/2767 (0 FAILED); JavaParsing* 105/105.
  • Result: green. Full write-up + reviewed-healthy caches + riskier follow-ups (plain-HashMap concurrency in JvmBinaryClassFinderInputsOverIndex, annotation classId memoization, more per-type/param lazies) in implDocs/PERFORMANCE_REVIEW_2026_07_20.md.

2026-07-20 — Read package-level default-nullability annotations off binary package-info.class

  • Change: the library-session facade‘s finder (was a no-op findPackage) now materialises a binary <pkg>/package-info.class and exposes its class-level annotations as the package’s JavaPackage.annotations. Previously those were dropped, so JSR-305/JSpecify package defaults (@ParametersAreNonnullByDefault, @TypeQualifierDefault, @NullMarked, …) on a binary Java package were invisible: a type-variable parameter substituted with an explicitly nullable Kotlin type argument stayed nullable instead of becoming definitely-non-null, producing a spurious UNSAFE_CALL in user code (dokka's Property<File?>.map { it.relativeToOrSelf(..) }). The finder reuses the same memoised binary index the deserializer reads through; class/package existence still routes through the deserializer, so only annotations are added.
  • Files: JvmBinaryClassFinderInputsOverIndex.kt (+findPackageInfoClass), JavaDirectFacadeBuilder.kt (NoOpJavaClassFinderLibraryJavaClassFinder + BinaryPackageInfoJavaPackage), cli-jvm/…/JvmFrontendPipelinePhase.kt (thread the shared binary-inputs builder into the facade builder); new test codegen/boxJvm/javaInterop/foreignAnnotationsTests/tests/dnnParameterFromBinaryPackageAnnotation.kt.
  • Tests: full box+phased suite green (0 FAILED). New reproducer fails without the fix with the exact reported symptom (UNSAFE_CALL … nullable receiver of type 'String?') and passes with it.
  • Result: regression fixed (java-direct now matches PSI on binary package defaults).

2026-07-16 — Remove the loose probeFqnSplits fallback: commit to the leftmost type like javac

  • Change: resolveQualifiedNameToClassIdFromParts no longer retries a failed name as a plain package.Class split. Like javac (JLS 6.5.4/6.5.5), once a leftmost type is found the interpretation is committed: a failed member-type descent returns the nonexistent nested id of the committed prefix (full resolution), which stays unresolved downstream — red code, exactly as javac reports on a package/type name clash (JLS 6.1). The reentrance-safe flavor returns null instead, so supertype-walk seeding is never poisoned by a dangling id. probeFqnSplits deleted.
  • Tests: the strict behavior conflicts with the PSI Java model (which loosely resolves the package interpretation), so the two tests pinning the loose behavior moved out of the shared roots: qualifiedNamePackageClassClash.kt deleted from the shared roots and recreated with javac-strict expectations in the new java-direct-owned testData/diagnostics root (wired into TestGenerator); the pre-existing javac/qualifiedExpression/PackageVsClass2.kt — verified against real javac to be red code (“cannot find symbol: class b, location: class a”) — is skipped via the new SkipTestsPinningPsiJavaModelDeviationsMetaConfigurator and mirrored strictly in the same root. Strict diagnostics: MISSING_DEPENDENCY_CLASS on the call whose Java signature uses the clash name + UNRESOLVED_REFERENCE on members of the unresolved type.
  • Files: resolution/JavaTypeResolver.kt (−32/+18), testFixtures/…/components.kt, testFixtures/…/AbstractJavaUsingAstTest.kt, testFixtures/…/TestGenerator.kt, build.gradle.kts (own testdata root registered); Scenario D refreshed in ReadMe.md and implDocs/RESOLUTION_SCHEMA.md.
  • Result: the module is now javac-conformant on qualified-name resolution — the last deliberate JLS deviation (KT-87813's unsound loose fallback) is gone. Full module suite green (box + phased, 0 FAILED; the skipped PSI-pinning test is mirrored strictly).

2026-07-16 — Rewrite resolveQualifiedNameToClassIdFromParts as a left-to-right JLS 6.5.4 pass

  • Change: replaced the recursive try-every-split loop (outer-prefix enumeration + per-prefix recursion, O(n²) probes) with a single non-recursive left-to-right pass that mirrors javac's PackageOrTypeName classification: first segment as a simple type name in scope (JLS 6.5.4.1), else grow the package prefix until a segment names a top-level type (JLS 6.5.4.2), then a member-type descent probing declared-then-inherited at every segment (JLS 6.5.5.2). The loose probeFqnSplits fallback survives, but is now reached only when the JLS pass fails — the sole divergence from javac remains the package/type name clash pinned by qualifiedNamePackageClassClash.kt (KT-87813).
  • Files: resolution/JavaTypeResolver.kt (−36/+28); Scenario D refreshed in ReadMe.md and implDocs/RESOLUTION_SCHEMA.md.
  • Tests: box+phased suite green (0 FAILED); JavaParsing* unit tests green.
  • Result: simplification landed (behavior-preserving on the whole suite).

2026-07-15 — Drop redundant JavaToKotlinClassMap disjunct in resolveFromJavaLang

  • Change: resolveFromJavaLang accepted a name when either JavaToKotlinClassMap.mapJavaToKotlin hit or classExists was true; the map disjunct is dead. It only ever probes ClassId(java.lang, X), and classExists resolves those via the symbol provider whose JVM builtins arm answers only kotlin.* ids (StandardClassIds.builtInsPackages), so a java.lang.* lookup never returns a BuiltIns-origin symbol filtered by tryResolve — it hits the JDK library class instead. Every mapped java.lang fqName (Object/String/Number/CharSequence/Comparable/Throwable/Cloneable/Iterable/Enum/wrappers/Deprecated) exists in both full JDK and mockJDK, so classExists already covers exactly what the map would. Collapsed to classExists(classId, fullResolution), matching resolveFromSamePackage; better PSI parity (no divergence in the no-JDK case). Removed the now-unused JavaToKotlinClassMap import.
  • Files: resolution/JavaTypeResolver.kt (−5/+1).
  • Tests: box+phased suite green (0 FAILED/0 errors).
  • Result: simplification landed (reviewer question — classExists alone suffices).

2026-07-15 — Nameless Java method recovered as a constructor (testNamelessInJava)

  • Change: JavaClassOverAst.constructors required a constructor METHOD node to have both no return TYPE and an IDENTIFIER, so a malformed nameless declaration like void () {} (its void is an error element, not a return type) was dropped; with no explicit constructor a public default constructor was synthesized, so class K : Nameless() saw a visible constructor and produced no diagnostic. PSI (same syntax parser) treats any no-return-type method as a (package-private) constructor, suppressing the default one → INVISIBLE_REFERENCE. Dropped the IDENTIFIER requirement so constructor detection mirrors PSI's getReturnTypeElement() == null.
  • Files: model/JavaClassOverAst.kt (constructors filter). Test data unchanged (shared golden).
  • Tests: full box+phased suite 2839/2839 (0 FAILED); JavaParsingTest/JavaLightTreeTest green.
  • Result: regression fixed (new test from master merge; SDK 261 golden).

2026-07-14 — Skip inaccessible inherited nested classes (IJ-FP regression: testIntellij_exceptionAnalyzer)

  • Change: walkSupertypeClassIds accepted the first inherited nested class of a matching simple name regardless of accessibility, so a package-private nested type in a supertype from another package (e.g. SimpleColoredComponent.TextRenderer) wrongly shadowed a same-named top-level class, producing a spurious RETURN_TYPE_MISMATCH. Now filtered by JLS 6.6/8.2 accessibility: private never inherited, package-private only within the declaring package; inaccessible matches expand deeper instead of resolving. Visibility read cycle-safely via new FirJavaClass.nonEnhancedVisibility (from originalStatus, no lazy status) surfaced through FirBackedJavaClassAdapter.visibility.
  • Files: resolution/JavaInheritedClassResolver.kt, resolution/FirBackedJavaClassAdapter.kt, fir-jvm/.../FirJavaClass.kt (+nonEnhancedVisibility); test codegen/box/javaDirect/packagePrivateInheritedNestedClassNotVisibleAcrossPackages.kt.
  • Tests: box 1179/1179, phased 1513/1513 (0 FAILED); IntelliJFullPipelineTestsGenerated.testIntellij_exceptionAnalyzer green under java-direct; PSI + CompileKotlinAgainstKotlin gates green (shared FirJavaClass edit).
  • Result: regression fixed.

2026-07-13 — Memoize JavaClassOverAst.supertypes (IJ-FP regression)

  • Change: supertypes was a recomputing get() that returned fresh JavaClassifierTypeOverAst instances on every read; FIR forces it from two lazy slots per class (FirJavaClass.superTypeRefs enhancement and directSupertypeClassIdsCache), so each supertype's classifier (a per-instance lazy hitting the symbol provider) resolved from cold twice. Made it by lazy(PUBLICATION) so both reads share instances and each supertype resolves once. List build allocates wrappers only → still resolution-safe.
  • Files: model/JavaClassOverAst.kt (supertypes get()→by lazy).
  • Tests: box 1178/1178, phased 1513/1513 (0 FAILED).
  • Result: regression fixed. IntelliJ testIntellij_platform_ide_impl warm frontend (isolated bench, 4 iters, same build): java-direct 21.5s wall / 20.8s CPU vs legacy 25.3s / 23.6s — was ~+8% before.