Skip to content

Commit 21c872a

Browse files
committed
feat(PackageManager): Support path excludes in findManagedFiles()
This allows skipping projects in specific paths already before starting the analysis. That way problematic / large projects that are irrelevant compliance-wise can be excluded leading to reduced resource usage and analysis time. Signed-off-by: Oliver Heger <[email protected]>
1 parent 08ccbda commit 21c872a

File tree

2 files changed

+89
-19
lines changed

2 files changed

+89
-19
lines changed

analyzer/src/main/kotlin/PackageManager.kt

+27-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import java.nio.file.Path
2727
import java.nio.file.SimpleFileVisitor
2828
import java.nio.file.attribute.BasicFileAttributes
2929

30+
import kotlin.io.path.pathString
3031
import kotlin.time.measureTime
3132

3233
import org.apache.logging.log4j.kotlin.Logging
@@ -93,20 +94,25 @@ abstract class PackageManager(
9394

9495
/**
9596
* Recursively search the [directory] for files managed by any of the [packageManagers]. The search is performed
96-
* depth-first so that root project files are found before any subproject files for a specific manager.
97+
* depth-first so that root project files are found before any subproject files for a specific manager. Path
98+
* excludes defined by the given [excludes] are taken into account; the corresponding directories are skipped.
9799
*/
98100
fun findManagedFiles(
99101
directory: File,
100-
packageManagers: Collection<PackageManagerFactory> = ALL.values
102+
packageManagers: Collection<PackageManagerFactory> = ALL.values,
103+
excludes: Excludes = Excludes.EMPTY
101104
): ManagedProjectFiles {
102105
require(directory.isDirectory) {
103106
"The provided path is not a directory: ${directory.absolutePath}"
104107
}
105108

109+
logger.debug { "Searching for managed files using the following excludes: $excludes" }
110+
106111
val result = mutableMapOf<PackageManagerFactory, MutableList<File>>()
112+
val rootPath = directory.toPath()
107113

108114
Files.walkFileTree(
109-
directory.toPath(),
115+
rootPath,
110116
object : SimpleFileVisitor<Path>() {
111117
override fun preVisitDirectory(dir: Path, attributes: BasicFileAttributes): FileVisitResult {
112118
if (IGNORED_DIRECTORY_MATCHERS.any { it.matches(dir) }) {
@@ -117,6 +123,14 @@ abstract class PackageManager(
117123
return FileVisitResult.SKIP_SUBTREE
118124
}
119125

126+
if (excludes.isPathExcluded(rootPath, dir)) {
127+
logger.info {
128+
"Not analyzing directory '$dir' as it is excluded."
129+
}
130+
131+
return FileVisitResult.SKIP_SUBTREE
132+
}
133+
120134
val dirAsFile = dir.toFile()
121135

122136
// Note that although FileVisitOption.FOLLOW_LINKS is not set, this would still follow junctions
@@ -126,7 +140,9 @@ abstract class PackageManager(
126140
return FileVisitResult.SKIP_SUBTREE
127141
}
128142

129-
val filesInDir = dirAsFile.walk().maxDepth(1).filter { it.isFile }.toList()
143+
val filesInDir = dirAsFile.walk().maxDepth(1).filter {
144+
it.isFile && !excludes.isPathExcluded(rootPath, it.toPath())
145+
}.toList()
130146

131147
packageManagers.distinct().forEach { manager ->
132148
// Create a list of lists of matching files per glob.
@@ -209,6 +225,13 @@ abstract class PackageManager(
209225
*/
210226
internal fun AnalyzerConfiguration.excludes(repositoryConfiguration: RepositoryConfiguration): Excludes =
211227
repositoryConfiguration.excludes.takeIf { skipExcludes } ?: Excludes.EMPTY
228+
229+
/**
230+
* Check whether the given [path] interpreted relatively against [root] is matched by a path exclude in this
231+
* [Excludes] object.
232+
*/
233+
private fun Excludes.isPathExcluded(root: Path, path: Path): Boolean =
234+
isPathExcluded(root.relativize(path).pathString)
212235
}
213236

214237
/**

analyzer/src/test/kotlin/PackageManagerTest.kt

+62-15
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ import io.kotest.matchers.maps.beEmpty
2929
import io.kotest.matchers.should
3030
import io.kotest.matchers.shouldBe
3131

32+
import java.io.File
33+
3234
import org.ossreviewtoolkit.analyzer.managers.*
3335
import org.ossreviewtoolkit.model.VcsInfo
3436
import org.ossreviewtoolkit.model.VcsType
37+
import org.ossreviewtoolkit.model.config.Excludes
38+
import org.ossreviewtoolkit.model.config.PathExclude
39+
import org.ossreviewtoolkit.model.config.PathExcludeReason
3540
import org.ossreviewtoolkit.utils.test.createSpecTempDir
41+
import org.ossreviewtoolkit.utils.test.createTestTempDir
3642

3743
class PackageManagerTest : WordSpec({
3844
val definitionFiles = listOf(
@@ -71,11 +77,7 @@ class PackageManagerTest : WordSpec({
7177
val projectDir = createSpecTempDir()
7278

7379
beforeSpec {
74-
definitionFiles.forEach { file ->
75-
projectDir.resolve(file).also { dir ->
76-
dir.parentFile.mkdirs()
77-
}.writeText("Dummy text to avoid the file to be empty, as empty files are skipped.")
78-
}
80+
definitionFiles.writeFiles(projectDir)
7981
}
8082

8183
"findManagedFiles" should {
@@ -88,11 +90,7 @@ class PackageManagerTest : WordSpec({
8890
it is Unmanaged.Factory
8991
}
9092

91-
// The keys in expected and actual maps of definition files are different instances of package manager
92-
// factories. So to compare values use the package manager types as keys instead.
93-
val managedFilesByName = managedFiles.map { (manager, files) ->
94-
manager.type to files.map { it.relativeTo(projectDir).invariantSeparatorsPath }
95-
}.toMap()
93+
val managedFilesByName = managedFiles.groupByName(projectDir)
9694

9795
assertSoftly {
9896
managedFilesByName["Bower"] should containExactly("bower/bower.json")
@@ -144,11 +142,7 @@ class PackageManagerTest : WordSpec({
144142

145143
managedFiles.size shouldBe 3
146144

147-
// The keys in expected and actual maps of definition files are different instances of package manager
148-
// factories. So to compare values use the package manager types as keys instead.
149-
val managedFilesByName = managedFiles.map { (manager, files) ->
150-
manager.type to files.map { it.relativeTo(projectDir).invariantSeparatorsPath }
151-
}.toMap()
145+
val managedFilesByName = managedFiles.groupByName(projectDir)
152146

153147
managedFilesByName["Gradle"] should containExactlyInAnyOrder(
154148
"gradle-groovy/build.gradle",
@@ -167,6 +161,38 @@ class PackageManagerTest : WordSpec({
167161
managedFiles should beEmpty()
168162
}
169163

164+
"take path excludes into account" {
165+
val tempDir = "test/"
166+
val definitionFilesWithExcludes = definitionFiles +
167+
listOf("pom.xml", "build.gradle", "build.sbt").map { "$tempDir$it" }
168+
val rootDir = createTestTempDir()
169+
definitionFilesWithExcludes.writeFiles(rootDir)
170+
171+
val pathExclude = PathExclude("$tempDir**", PathExcludeReason.TEST_OF)
172+
val excludes = Excludes(paths = listOf(pathExclude))
173+
174+
val managedFilesByName = PackageManager.findManagedFiles(rootDir, excludes = excludes).groupByName(rootDir)
175+
176+
managedFilesByName["Gradle"] should containExactlyInAnyOrder(
177+
"gradle-groovy/build.gradle",
178+
"gradle-kotlin/build.gradle.kts"
179+
)
180+
managedFilesByName["Maven"] should containExactly("maven/pom.xml")
181+
managedFilesByName["SBT"] should containExactly("sbt/build.sbt")
182+
}
183+
184+
"handle specific excluded definition files" {
185+
val pathExclude = PathExclude("gradle-groovy/build.gradle", PathExcludeReason.OTHER)
186+
val excludes = Excludes(paths = listOf(pathExclude))
187+
188+
val managedFiles = PackageManager.findManagedFiles(projectDir, excludes = excludes)
189+
val managedFilesByName = managedFiles.groupByName(projectDir)
190+
191+
managedFilesByName["Gradle"] should containExactly(
192+
"gradle-kotlin/build.gradle.kts"
193+
)
194+
}
195+
170196
"fail if the provided file is not a directory" {
171197
shouldThrow<IllegalArgumentException> {
172198
PackageManager.findManagedFiles(projectDir.resolve("pom.xml"))
@@ -221,3 +247,24 @@ class PackageManagerTest : WordSpec({
221247
}
222248
}
223249
})
250+
251+
/**
252+
* Transform this map with definition files grouped by package manager factories, so that the results of specific
253+
* package managers can be easily accessed. The keys in expected and actual maps of definition files are different
254+
* instances of package manager factories. So to compare values use the package manager types as keys instead.
255+
*/
256+
private fun ManagedProjectFiles.groupByName(projectDir: File) =
257+
map { (manager, files) ->
258+
manager.type to files.map { it.relativeTo(projectDir).invariantSeparatorsPath }
259+
}.toMap()
260+
261+
/**
262+
* Create files with a dummy content in the given [directory] for all the path names in this collection.
263+
*/
264+
private fun Collection<String>.writeFiles(directory: File) {
265+
forEach { file ->
266+
directory.resolve(file).also { dir ->
267+
dir.parentFile.mkdirs()
268+
}.writeText("Dummy text to avoid the file to be empty, as empty files are skipped.")
269+
}
270+
}

0 commit comments

Comments
 (0)