@@ -100,9 +100,9 @@ internal class KtSymbolIndex(
100100 private val refreshExecutor: ExecutorService =
101101 Executors .newFixedThreadPool(2 ) { r -> Thread (r, " KtCurrentFileRefresh" ).apply { isDaemon = true } }
102102
103- /* * path -> in-flight/last-launched refresh. Guarded by currentFiles.asMap().compute per key. */
103+ /* * path -> in-flight/last-launched refresh; mutated only inside the per- key `compute` below . */
104104 private val currentFiles = ConcurrentHashMap <Path , CompletableFuture <VersionedKtFile >>()
105- /* * path -> version most recently launched. Read /written only inside the compute() critical section. */
105+ /* * path -> last- launched version; read /written only inside that same `compute` section. */
106106 private val currentVersions = ConcurrentHashMap <Path , Int >()
107107
108108 fun syncIndexInBackground () {
@@ -204,34 +204,26 @@ internal class KtSymbolIndex(
204204 return future.thenApply { it.ktFile }
205205 }
206206
207- /* * Parses the live document for [path], registers it as the in-memory file, returns the stamped file. */
207+ /* *
208+ * Parses [path]'s live document into a fresh [KtFile], registers it as the in-memory file, and
209+ * transitions the module's FIR session (invalidate + reindex) so later analysis sees the content.
210+ *
211+ * The result is stamped with [version] (captured when the refresh was launched) even though the
212+ * content is read later, here. FileManager writes version-before-content unsynchronized, so the
213+ * stamp may lag the content but never lead it: callers never get older-than-requested content, and
214+ * a lagging stamp only costs one redundant re-parse on the next request.
215+ */
208216 private fun refreshToCurrent (path : Path , version : Int , old : KtFile ? ): VersionedKtFile {
209- // [version] is the version observed (by getCurrentKtFile, reading ActiveDocument.version)
210- // when this refresh was launched inside compute(); the content below is read live, possibly
211- // later (this runs on refreshExecutor, after any prior refresh's prior.get()).
212- // FileManager.onDocumentContentChange writes `version` before `content`, and neither field is
213- // volatile/synchronized (a pre-existing race, not introduced here) — so a reader can observe a
214- // `version` that is older than the `content` it later reads, never the reverse. Combined with
215- // content advancing monotonically, this means what we parse here is always at-least-as-fresh
216- // as the stamped [version]: callers never see stale (older-than-requested) content. The only
217- // effect of the skew is a spurious cache miss and one redundant re-parse on the next request
218- // for the newer version.
219217 val content = FileManager .getDocumentContents(path)
220218 val newKtFile = project.read { parser.createFile(path.pathString, content) }
221219 newKtFile.backingFilePath = path
222- // Use the view provider's virtual file rather than KtFile.virtualFile: the latter is null for
223- // non-physical PSI files (parser event system disabled, e.g. the unit-test environment), while
224- // the view provider always exposes the backing light virtual file. For physical files (prod)
225- // the two are the same object.
220+ // KtFile.virtualFile is null for non-physical PSI (unit-test env); the view provider's is
221+ // always present, and identical to it in production.
226222 ProjectStructureProvider .getInstance(project)
227223 .registerInMemoryFile(path.pathString, newKtFile.viewProvider.virtualFile)
228- // Mirrors CompilationEnvironment.onFileContentChanged: invalidate the FIR session's view of
229- // the (old) element under the write lock so it can't race a concurrent `analyze` (which only
230- // holds the read lock), then re-index the new instance.
224+ // project.write serializes the mutation against a concurrent `analyze` (read lock); runWriteAction
225+ // supplies the platform write access handleElementModification asserts, which our RW lock does not.
231226 project.write {
232- // handleElementModification publishes an out-of-block modification event, which the
233- // platform's ThreadingAssertions require to run inside a write action (independent of our
234- // own read/write lock above, which only serializes with `analyze`).
235227 ApplicationManager .getApplication().runWriteAction {
236228 KaSourceModificationService .getInstance(project)
237229 .handleElementModification(old ? : newKtFile, KaElementModificationType .Unknown )
@@ -263,13 +255,9 @@ internal class KtSymbolIndex(
263255 if (! DocumentUtils .isKotlinFile(path)) return null
264256
265257 if (FileManager .isActive(path)) {
266- // Active document: peek the current-file cache without blocking. Calling
267- // getCurrentKtFile(path).get() here would deadlock: getKtFile is invoked by
268- // Analysis-API services (DeclarationsProvider, AnnotationsResolver,
269- // DirectInheritorsProvider) while FIR resolution holds project.read, and a blocking
270- // refresh needs project.write on the executor thread (reader-holds-lock waits on
271- // writer -> deadlock). A peek miss falls back to the disk instance below; the file's
272- // own refresh is already scheduled on edit and will be served on the next request.
258+ // Peek, never block: getKtFile runs under project.read inside Analysis-API services, so a
259+ // blocking getCurrentKtFile().get() (its refresh needs project.write) would deadlock. A miss
260+ // falls through to the disk instance; the edit already scheduled a refresh for next time.
273261 getCurrentKtFileIfPresent(path)?.let { return it }
274262 }
275263
@@ -309,9 +297,8 @@ internal class KtSymbolIndex(
309297 // (APPDEVFORALL-17R). This index owns `scope`.
310298 scope.coroutineContext[Job ]?.cancelAndJoin()
311299
312- // Drain the current-file refresh pool: refreshToCurrent runs project.read/write on it, so no
313- // in-flight refresh may survive into the caller's project disposal (same rationale as the
314- // scope join above). Bounded so a slow parse can't block shutdown indefinitely.
300+ // Drain the refresh pool before disposal: refreshToCurrent runs project.read/write on it (same
301+ // rationale as the scope join above). Bounded so a slow parse can't stall shutdown.
315302 refreshExecutor.shutdownNow()
316303 refreshExecutor.awaitTermination(CLOSE_DRAIN_TIMEOUT_SECONDS , TimeUnit .SECONDS )
317304 }
0 commit comments