IJPL-250821 fix ModuleFilesIteratorImpl misses files under inner CONTENT_NON_INDEXABLE filesets

WorkspaceFileIndexImpl.processContentUnderDirectory calls getFileInfo to find filesets. It searches for both CONTENT and CONTENT_NON_INDEXABLE filesets

However, ModuleFilesIteratorImpl passes fileset filter, checking that filesets belong to the module.

When inner non-indexable root does not belong to the module (almost always), the iterator skip file as not indexable, instead of taking it

(cherry picked from commit e7e69a64d85b0426175376ec5a4cc4796d00da1a)

GitOrigin-RevId: 260a2a9503338bd83fdc11d4d818854cb7f0d09f
This commit is contained in:
Ilia Malakhov
2026-07-23 15:32:43 +00:00
committed by intellij-monorepo-bot
parent d6ff41aa4e
commit febef8303a
2 changed files with 127 additions and 2 deletions
@@ -16,8 +16,12 @@ import com.intellij.workspaceModel.core.fileIndex.WorkspaceFileIndex.Companion.g
import com.intellij.workspaceModel.core.fileIndex.WorkspaceFileSetWithCustomData
import com.intellij.workspaceModel.core.fileIndex.impl.ModuleRelatedRootData
import com.intellij.workspaceModel.core.fileIndex.impl.WorkspaceFileIndexEx
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
internal class ModuleFilesIteratorImpl(
@ApiStatus.Internal
@VisibleForTesting
class ModuleFilesIteratorImpl(
private val module: Module,
private val root: VirtualFile,
private val recursive: Boolean,
@@ -61,7 +65,12 @@ internal class ModuleFilesIteratorImpl(
val myWorkspaceFileIndex = getInstance(project) as WorkspaceFileIndexEx
return if (recursive) {
iterateContentUnderDirectory(root, processorEx, fileFilter, myWorkspaceFileIndex)
val customFilter = VirtualFileFilter { file ->
val info = myWorkspaceFileIndex.getFileInfo(file, true, true, false, false, false ,false, false)
info.findFileSet { it.kind.isIndexable && (it.data as? ModuleRelatedRootData)?.module == module } != null
}.and(fileFilter) // run `fileFilter` second, so if it is deduplication filter, it won't be invoked on files outside module
val fileSetFilter: (WorkspaceFileSetWithCustomData<*>) -> Boolean = { fileSet -> !isScopeDisposed() && fileSet.kind.isContent }
myWorkspaceFileIndex.processContentUnderDirectory(root, processorEx, customFilter, fileSetFilter)
}
else {
fileFilter.accept(root) && processorEx.processFileEx(root) != TreeNodeProcessingResult.STOP
@@ -0,0 +1,116 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.roots.impl.indexing
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.readAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.roots.ContentIterator
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileFilter
import com.intellij.platform.backend.workspace.toVirtualFileUrl
import com.intellij.platform.backend.workspace.workspaceModel
import com.intellij.platform.workspace.jps.entities.ContentRootEntity
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import com.intellij.testFramework.IndexingTestUtil
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.rules.ProjectModelExtension
import com.intellij.testFramework.rules.TempDirectoryExtension
import com.intellij.testFramework.workspaceModel.update
import com.intellij.util.indexing.roots.IndexableFilesIterator
import com.intellij.util.indexing.roots.ModuleFilesIteratorImpl
import com.intellij.util.indexing.testEntities.NonIndexableKindFileSetTestContributor
import com.intellij.util.indexing.testEntities.NonIndexableTestEntity
import com.intellij.workspaceModel.core.fileIndex.impl.WorkspaceFileIndexImpl
import com.intellij.workspaceModel.ide.NonPersistentEntitySource
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@TestApplication
internal class ModuleFilesIteratorImplTest {
@RegisterExtension
private val projectModel = ProjectModelExtension()
@RegisterExtension
private val tempDir = TempDirectoryExtension()
private val disposable: Disposable get() = projectModel.disposableRule.disposable
private val project get() = projectModel.project
private val workspaceModel get() = project.workspaceModel
private val virtualFileUrlManager get() = workspaceModel.getVirtualFileUrlManager()
private lateinit var module: Module
private lateinit var moduleRoot: VirtualFile
private lateinit var innerRoot: VirtualFile
@BeforeEach
fun setUp(): Unit = runBlocking {
WorkspaceFileIndexImpl.EP_NAME.point.registerExtension(NonIndexableKindFileSetTestContributor(), disposable)
moduleRoot = tempDir.newVirtualDirectory("root")
tempDir.newVirtualFile("root/root.txt")
innerRoot = tempDir.newVirtualDirectory("root/inner")
tempDir.newVirtualFile("root/inner/inner.txt")
workspaceModel.update { storage ->
storage.addEntity(ModuleEntity("module", emptyList(), NonPersistentEntitySource) {
contentRoots = listOf(ContentRootEntity(moduleRoot.toVirtualFileUrl(), emptyList(), NonPersistentEntitySource))
})
}
IndexingTestUtil.waitUntilIndexesAreReady(project)
module = ModuleManager.getInstance(project).modules.single()
}
@Test
fun `iterates module root without an inner non-indexable fileset`(): Unit = runBlocking {
assertIteratesModuleRoot()
}
@Test
fun `iterates module root with an inner non-indexable fileset`(): Unit = runBlocking {
workspaceModel.update { storage ->
storage.addEntity(NonIndexableTestEntity(innerRoot.toVirtualFileUrl(), NonPersistentEntitySource))
}
IndexingTestUtil.waitUntilIndexesAreReady(project)
assertIteratesModuleRoot()
}
private suspend fun assertIteratesModuleRoot() {
val files = readAction {
val result = mutableListOf<VirtualFile>()
val fullyProcessed = moduleFilesIterator().iterateFiles(
project,
ContentIterator { file ->
result.add(file)
true
},
VirtualFileFilter.ALL,
)
assertThat(fullyProcessed).isTrue()
result
}
assertThat(relativePaths(files)).containsExactlyInAnyOrder("", "root.txt", "inner", "inner/inner.txt")
}
private fun moduleFilesIterator(): IndexableFilesIterator =
ModuleFilesIteratorImpl(module, moduleRoot, true, true)
private fun relativePaths(files: Collection<VirtualFile>): Set<String> {
return files.mapTo(linkedSetOf()) { file ->
VfsUtilCore.getRelativePath(file, moduleRoot, '/') ?: ""
}
}
private fun VirtualFile.toVirtualFileUrl(): VirtualFileUrl = this.toVirtualFileUrl(virtualFileUrlManager)
}