diff --git a/platform/lang-impl/src/com/intellij/util/indexing/roots/ModuleFilesIteratorImpl.kt b/platform/lang-impl/src/com/intellij/util/indexing/roots/ModuleFilesIteratorImpl.kt index 45857e3c0940..bf288e4383cf 100644 --- a/platform/lang-impl/src/com/intellij/util/indexing/roots/ModuleFilesIteratorImpl.kt +++ b/platform/lang-impl/src/com/intellij/util/indexing/roots/ModuleFilesIteratorImpl.kt @@ -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 diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/roots/impl/indexing/ModuleFilesIteratorImplTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/roots/impl/indexing/ModuleFilesIteratorImplTest.kt new file mode 100644 index 000000000000..40124383b76f --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/roots/impl/indexing/ModuleFilesIteratorImplTest.kt @@ -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() + 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): Set { + return files.mapTo(linkedSetOf()) { file -> + VfsUtilCore.getRelativePath(file, moduleRoot, '/') ?: "" + } + } + + private fun VirtualFile.toVirtualFileUrl(): VirtualFileUrl = this.toVirtualFileUrl(virtualFileUrlManager) +}