diff --git a/plugins/package-managers/node/src/main/kotlin/NodePackageManager.kt b/plugins/package-managers/node/src/main/kotlin/NodePackageManager.kt new file mode 100644 index 0000000000000..85339c83e0664 --- /dev/null +++ b/plugins/package-managers/node/src/main/kotlin/NodePackageManager.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagemanagers.node + +import java.io.File + +import org.apache.logging.log4j.kotlin.logger + +import org.ossreviewtoolkit.analyzer.PackageManager +import org.ossreviewtoolkit.analyzer.PackageManagerResult +import org.ossreviewtoolkit.analyzer.parseAuthorString +import org.ossreviewtoolkit.downloader.VersionControlSystem +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.Project +import org.ossreviewtoolkit.model.ProjectAnalyzerResult +import org.ossreviewtoolkit.model.config.AnalyzerConfiguration +import org.ossreviewtoolkit.model.config.RepositoryConfiguration +import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder +import org.ossreviewtoolkit.utils.common.realFile + +abstract class NodePackageManager( + managerName: String, + val managerType: NodePackageManagerType, + analysisRoot: File, + analyzerConfig: AnalyzerConfiguration, + repoConfig: RepositoryConfiguration +) : PackageManager(managerName, managerType.projectType, analysisRoot, analyzerConfig, repoConfig) { + protected abstract val graphBuilder: DependencyGraphBuilder<*> + + protected fun parseProject(packageJsonFile: File, analysisRoot: File): Project { + logger.debug { "Parsing project info from '$packageJsonFile'." } + + val packageJson = parsePackageJson(packageJsonFile) + + val rawName = packageJson.name.orEmpty() + val (namespace, name) = splitNamespaceAndName(rawName) + + val projectName = name.ifBlank { + getFallbackProjectName(analysisRoot, packageJsonFile).also { + logger.warn { "'$packageJsonFile' does not define a name, falling back to '$it'." } + } + } + + val version = packageJson.version.orEmpty() + if (version.isBlank()) { + logger.warn { "'$packageJsonFile' does not define a version." } + } + + val declaredLicenses = packageJson.licenses.mapLicenses() + val authors = packageJson.authors.flatMap { parseAuthorString(it.name) } + .mapNotNullTo(mutableSetOf()) { it.name } + val description = packageJson.description.orEmpty() + val homepageUrl = packageJson.homepage.orEmpty() + val projectDir = packageJsonFile.parentFile.realFile() + val vcsFromPackage = parseVcsInfo(packageJson) + + return Project( + id = Identifier( + type = projectType, + namespace = namespace, + name = projectName, + version = version + ), + definitionFilePath = VersionControlSystem.getPathInfo(packageJsonFile.realFile()).path, + authors = authors, + declaredLicenses = declaredLicenses, + vcs = vcsFromPackage, + vcsProcessed = processProjectVcs(projectDir, vcsFromPackage, homepageUrl), + description = description, + homepageUrl = homepageUrl + ) + } + + override fun mapDefinitionFiles(definitionFiles: List) = + NodePackageManagerDetection(definitionFiles).filterApplicable(managerType) + + override fun createPackageManagerResult(projectResults: Map>) = + PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) +} diff --git a/plugins/package-managers/node/src/main/kotlin/NpmDetection.kt b/plugins/package-managers/node/src/main/kotlin/NodePackageManagerDetection.kt similarity index 92% rename from plugins/package-managers/node/src/main/kotlin/NpmDetection.kt rename to plugins/package-managers/node/src/main/kotlin/NodePackageManagerDetection.kt index e07a1d6e5f763..3bba34b236b76 100644 --- a/plugins/package-managers/node/src/main/kotlin/NpmDetection.kt +++ b/plugins/package-managers/node/src/main/kotlin/NodePackageManagerDetection.kt @@ -31,15 +31,15 @@ import org.ossreviewtoolkit.utils.common.collectMessages /** * A class to detect the package managers used for the give [definitionFiles]. */ -internal class NpmDetection(private val definitionFiles: Collection) { +internal class NodePackageManagerDetection(private val definitionFiles: Collection) { /** * A map of project directories to the set of package managers that are most likely responsible for the project. If * the set is empty, none of the package managers is responsible. */ - private val projectDirManagers: Map> by lazy { + private val projectDirManagers: Map> by lazy { definitionFiles.associate { file -> val projectDir = file.parentFile - projectDir to NodePackageManager.forDirectory(projectDir) + projectDir to NodePackageManagerType.forDirectory(projectDir) } } @@ -50,7 +50,7 @@ internal class NpmDetection(private val definitionFiles: Collection) { private val workspacePatterns: Map> by lazy { definitionFiles.associate { file -> val projectDir = file.parentFile - val patterns = NodePackageManager.entries.mapNotNull { it.getWorkspaces(projectDir) }.flatten() + val patterns = NodePackageManagerType.entries.mapNotNull { it.getWorkspaces(projectDir) }.flatten() projectDir to patterns.map { FileSystems.getDefault().getPathMatcher("glob:${it.removeSuffix("/")}") } @@ -70,7 +70,7 @@ internal class NpmDetection(private val definitionFiles: Collection) { /** * Return those [definitionFiles] that define root projects for the given [manager]. */ - fun filterApplicable(manager: NodePackageManager): List = + fun filterApplicable(manager: NodePackageManagerType): List = definitionFiles.filter { file -> val projectDir = file.parentFile @@ -103,19 +103,21 @@ internal class NpmDetection(private val definitionFiles: Collection) { "Any of $managersFromFiles could be the package manager for '$file'. Assuming it is an NPM project." } - manager == NodePackageManager.NPM + manager == NodePackageManagerType.NPM } } /** * An enum of all supported Node package managers. */ -internal enum class NodePackageManager( +enum class NodePackageManagerType( + val projectType: String, val lockfileName: String, val markerFileName: String? = null, - val workspaceFileName: String = NodePackageManager.DEFINITION_FILE + val workspaceFileName: String = NodePackageManagerType.DEFINITION_FILE ) { NPM( + projectType = "NPM", lockfileName = "package-lock.json", // See https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json. markerFileName = "npm-shrinkwrap.json" // See https://docs.npmjs.com/cli/v6/configuring-npm/shrinkwrap-json. ) { @@ -124,6 +126,7 @@ internal enum class NodePackageManager( }, PNPM( + projectType = "PNPM", lockfileName = "pnpm-lock.yaml", // See https://pnpm.io/git#lockfiles. workspaceFileName = "pnpm-workspace.yaml" ) { @@ -142,6 +145,7 @@ internal enum class NodePackageManager( }, YARN( + projectType = "Yarn", lockfileName = "yarn.lock" // See https://classic.yarnpkg.com/en/docs/yarn-lock. ) { private val lockfileMarker = "# yarn lockfile v1" @@ -157,6 +161,7 @@ internal enum class NodePackageManager( }, YARN2( + projectType = "Yarn2", lockfileName = "yarn.lock", // See https://classic.yarnpkg.com/en/docs/yarn-lock. markerFileName = ".yarnrc.yml" ) { @@ -186,8 +191,8 @@ internal enum class NodePackageManager( /** * Return the set of package managers that are most likely responsible for the given [projectDir]. */ - fun forDirectory(projectDir: File): Set { - val scores = NodePackageManager.entries.associateWith { + fun forDirectory(projectDir: File): Set { + val scores = NodePackageManagerType.entries.associateWith { it.getFileScore(projectDir) } diff --git a/plugins/package-managers/node/src/main/kotlin/NpmSupport.kt b/plugins/package-managers/node/src/main/kotlin/NodePackageManagerSupport.kt similarity index 81% rename from plugins/package-managers/node/src/main/kotlin/NpmSupport.kt rename to plugins/package-managers/node/src/main/kotlin/NodePackageManagerSupport.kt index 43b0d23ee4b95..da341f9a6320d 100644 --- a/plugins/package-managers/node/src/main/kotlin/NpmSupport.kt +++ b/plugins/package-managers/node/src/main/kotlin/NodePackageManagerSupport.kt @@ -20,31 +20,21 @@ package org.ossreviewtoolkit.plugins.packagemanagers.node import java.io.File -import java.lang.invoke.MethodHandles -import org.apache.logging.log4j.kotlin.loggerOf - -import org.ossreviewtoolkit.analyzer.PackageManager.Companion.getFallbackProjectName import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processPackageVcs -import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processProjectVcs import org.ossreviewtoolkit.analyzer.parseAuthorString import org.ossreviewtoolkit.downloader.VcsHost -import org.ossreviewtoolkit.downloader.VersionControlSystem import org.ossreviewtoolkit.model.Hash import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.Package -import org.ossreviewtoolkit.model.Project import org.ossreviewtoolkit.model.RemoteArtifact import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType -import org.ossreviewtoolkit.utils.common.realFile import org.ossreviewtoolkit.utils.common.toUri import org.ossreviewtoolkit.utils.spdx.SpdxConstants internal const val NON_EXISTING_SEMVER = "0.0.0" -private val logger = loggerOf(MethodHandles.lookup().lookupClass()) - /** * Expand an NPM shortcut [url] to a regular URL as used for dependencies, see * https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies. @@ -228,49 +218,6 @@ internal fun parsePackage(packageJsonFile: File, getPackageDetails: GetPackageDe return module } -internal fun parseProject(packageJsonFile: File, analysisRoot: File, managerName: String): Project { - logger.debug { "Parsing project info from '$packageJsonFile'." } - - val packageJson = parsePackageJson(packageJsonFile) - - val rawName = packageJson.name.orEmpty() - val (namespace, name) = splitNamespaceAndName(rawName) - - val projectName = name.ifBlank { - getFallbackProjectName(analysisRoot, packageJsonFile).also { - logger.warn { "'$packageJsonFile' does not define a name, falling back to '$it'." } - } - } - - val version = packageJson.version.orEmpty() - if (version.isBlank()) { - logger.warn { "'$packageJsonFile' does not define a version." } - } - - val declaredLicenses = packageJson.licenses.mapLicenses() - val authors = packageJson.authors.flatMap { parseAuthorString(it.name) }.mapNotNullTo(mutableSetOf()) { it.name } - val description = packageJson.description.orEmpty() - val homepageUrl = packageJson.homepage.orEmpty() - val projectDir = packageJsonFile.parentFile.realFile() - val vcsFromPackage = parseVcsInfo(packageJson) - - return Project( - id = Identifier( - type = managerName, - namespace = namespace, - name = projectName, - version = version - ), - definitionFilePath = VersionControlSystem.getPathInfo(packageJsonFile.realFile()).path, - authors = authors, - declaredLicenses = declaredLicenses, - vcs = vcsFromPackage, - vcsProcessed = processProjectVcs(projectDir, vcsFromPackage, homepageUrl), - description = description, - homepageUrl = homepageUrl - ) -} - /** * Split the given [rawName] of a module to a pair with namespace and name. */ diff --git a/plugins/package-managers/node/src/main/kotlin/npm/ModuleInfo.kt b/plugins/package-managers/node/src/main/kotlin/npm/ModuleInfo.kt index 64d588836bfa5..59f949b618db9 100644 --- a/plugins/package-managers/node/src/main/kotlin/npm/ModuleInfo.kt +++ b/plugins/package-managers/node/src/main/kotlin/npm/ModuleInfo.kt @@ -31,7 +31,7 @@ internal fun parseNpmList(json: String): ModuleInfo = JSON.decodeFromString(json * Module information for installed NPM packages. */ @Serializable -internal data class ModuleInfo( +data class ModuleInfo( /** The name of the package. */ val name: String? = null, diff --git a/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt b/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt index f6d5298f85344..3dd79de32223c 100644 --- a/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt +++ b/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt @@ -25,8 +25,6 @@ import java.util.LinkedList import org.apache.logging.log4j.kotlin.logger import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory -import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.analyzer.PackageManagerResult import org.ossreviewtoolkit.model.Issue import org.ossreviewtoolkit.model.Project import org.ossreviewtoolkit.model.ProjectAnalyzerResult @@ -36,10 +34,9 @@ import org.ossreviewtoolkit.model.config.PackageManagerConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson -import org.ossreviewtoolkit.plugins.packagemanagers.node.parseProject import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.ProcessCapture @@ -70,14 +67,14 @@ class Npm( analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration -) : PackageManager(name, "NPM", analysisRoot, analyzerConfig, repoConfig) { +) : NodePackageManager(name, NodePackageManagerType.NPM, analysisRoot, analyzerConfig, repoConfig) { companion object { /** Name of the configuration option to toggle legacy peer dependency support. */ const val OPTION_LEGACY_PEER_DEPS = "legacyPeerDeps" } class Factory : AbstractPackageManagerFactory("NPM") { - override val globsForDefinitionFiles = listOf(NodePackageManager.DEFINITION_FILE) + override val globsForDefinitionFiles = listOf(NodePackageManagerType.DEFINITION_FILE) override fun create( analysisRoot: File, @@ -89,7 +86,8 @@ class Npm( private val legacyPeerDeps = options[OPTION_LEGACY_PEER_DEPS].toBoolean() private val npmViewCache = mutableMapOf() private val handler = NpmDependencyHandler(projectType, this::getRemotePackageDetails) - private val graphBuilder by lazy { DependencyGraphBuilder(handler) } + + override val graphBuilder by lazy { DependencyGraphBuilder(handler) } override fun resolveDependencies(definitionFile: File, labels: Map): List = stashDirectories(definitionFile.resolveSibling("node_modules")).use { @@ -102,7 +100,7 @@ class Npm( if (issues.any { it.severity == Severity.ERROR }) { val project = runCatching { - parseProject(definitionFile, analysisRoot, managerName) + parseProject(definitionFile, analysisRoot) }.getOrElse { logger.error { "Failed to parse project information: ${it.collectMessages()}" } Project.EMPTY @@ -111,7 +109,7 @@ class Npm( return listOf(ProjectAnalyzerResult(project, emptySet(), issues)) } - val project = parseProject(definitionFile, analysisRoot, managerName) + val project = parseProject(definitionFile, analysisRoot) val projectModuleInfo = listModules(workingDir, issues).undoDeduplication() val scopeNames = Scope.entries @@ -131,20 +129,12 @@ class Npm( ).let { listOf(it) } } - private fun hasLockfile(projectDir: File) = NodePackageManager.NPM.hasLockfile(projectDir) - - override fun mapDefinitionFiles(definitionFiles: List) = - NpmDetection(definitionFiles).filterApplicable(NodePackageManager.NPM) - override fun beforeResolution(definitionFiles: List) { // We do not actually depend on any features specific to an NPM version, but we still want to stick to a // fixed minor version to be sure to get consistent results. NpmCommand.checkVersion() } - override fun createPackageManagerResult(projectResults: Map>) = - PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) - private fun listModules(workingDir: File, issues: MutableList): ModuleInfo { val listProcess = NpmCommand.run(workingDir, "list", "--depth", "Infinity", "--json", "--long") issues += listProcess.extractNpmIssues() @@ -167,7 +157,7 @@ class Npm( } private fun installDependencies(workingDir: File): List { - requireLockfile(workingDir) { hasLockfile(workingDir) } + requireLockfile(workingDir) { managerType.hasLockfile(workingDir) } val options = listOfNotNull( "--ignore-scripts", @@ -175,7 +165,7 @@ class Npm( "--legacy-peer-deps".takeIf { legacyPeerDeps } ) - val subcommand = if (hasLockfile(workingDir)) "ci" else "install" + val subcommand = if (managerType.hasLockfile(workingDir)) "ci" else "install" val process = NpmCommand.run(workingDir, subcommand, *options.toTypedArray()) diff --git a/plugins/package-managers/node/src/main/kotlin/npm/NpmDependencyHandler.kt b/plugins/package-managers/node/src/main/kotlin/npm/NpmDependencyHandler.kt index 7c8acbcf451ea..12337565abaaf 100644 --- a/plugins/package-managers/node/src/main/kotlin/npm/NpmDependencyHandler.kt +++ b/plugins/package-managers/node/src/main/kotlin/npm/NpmDependencyHandler.kt @@ -27,7 +27,7 @@ import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.utils.DependencyHandler import org.ossreviewtoolkit.plugins.packagemanagers.node.GetPackageDetailsFun -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackage import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson @@ -75,7 +75,7 @@ private val ModuleInfo.isProject: Boolean get() = resolved == null private val ModuleInfo.packageJsonFile: File get() = File( checkNotNull(path) { - "The path to '${NodePackageManager.DEFINITION_FILE}' is null in $this." + "The path to '${NodePackageManagerType.DEFINITION_FILE}' is null in $this." }, - NodePackageManager.DEFINITION_FILE + NodePackageManagerType.DEFINITION_FILE ) diff --git a/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt b/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt index 7b9df7cab8ebe..72c906c1ead08 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt @@ -27,7 +27,7 @@ private val JSON = Json { ignoreUnknownKeys = true } internal fun parsePnpmList(json: String): List = JSON.decodeFromString(json) @Serializable -internal data class ModuleInfo( +data class ModuleInfo( val name: String? = null, val version: String? = null, val path: String, diff --git a/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt b/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt index 50cdbb755c844..7b2a1b8b028c5 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt @@ -24,17 +24,14 @@ import java.io.File import org.apache.logging.log4j.kotlin.logger import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory -import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.analyzer.PackageManagerResult import org.ossreviewtoolkit.model.ProjectAnalyzerResult import org.ossreviewtoolkit.model.config.AnalyzerConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson -import org.ossreviewtoolkit.plugins.packagemanagers.node.parseProject import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.stashDirectories @@ -56,9 +53,9 @@ class Pnpm( analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration -) : PackageManager(name, "PNPM", analysisRoot, analyzerConfig, repoConfig) { +) : NodePackageManager(name, NodePackageManagerType.PNPM, analysisRoot, analyzerConfig, repoConfig) { class Factory : AbstractPackageManagerFactory("PNPM") { - override val globsForDefinitionFiles = listOf(NodePackageManager.DEFINITION_FILE, "pnpm-lock.yaml") + override val globsForDefinitionFiles = listOf(NodePackageManagerType.DEFINITION_FILE, "pnpm-lock.yaml") override fun create( analysisRoot: File, @@ -67,9 +64,10 @@ class Pnpm( ) = Pnpm(type, analysisRoot, analyzerConfig, repoConfig) } - private val handler = PnpmDependencyHandler(projectType, this::getRemotePackageDetails) - private val graphBuilder by lazy { DependencyGraphBuilder(handler) } private val packageDetailsCache = mutableMapOf() + private val handler = PnpmDependencyHandler(projectType, this::getRemotePackageDetails) + + override val graphBuilder by lazy { DependencyGraphBuilder(handler) } override fun resolveDependencies(definitionFile: File, labels: Map): List = stashDirectories(definitionFile.resolveSibling("node_modules")).use { @@ -87,8 +85,8 @@ class Pnpm( val moduleInfosForScope = scopes.associateWith { scope -> listModules(workingDir, scope) } return workspaceModuleDirs.map { projectDir -> - val packageJsonFile = projectDir.resolve(NodePackageManager.DEFINITION_FILE) - val project = parseProject(packageJsonFile, analysisRoot, managerName) + val packageJsonFile = projectDir.resolve(NodePackageManagerType.DEFINITION_FILE) + val project = parseProject(packageJsonFile, analysisRoot) val scopeNames = scopes.mapTo(mutableSetOf()) { scope -> val scopeName = scope.descriptor @@ -126,12 +124,6 @@ class Pnpm( return parsePnpmList(json) } - override fun createPackageManagerResult(projectResults: Map>) = - PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) - - override fun mapDefinitionFiles(definitionFiles: List) = - NpmDetection(definitionFiles).filterApplicable(NodePackageManager.PNPM) - private fun installDependencies(workingDir: File) = PnpmCommand.run( "install", diff --git a/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt b/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt index 3e28e5828dce7..782d3db4f1e3b 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt @@ -27,7 +27,7 @@ import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.utils.DependencyHandler import org.ossreviewtoolkit.plugins.packagemanagers.node.GetPackageDetailsFun -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackage import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson @@ -80,7 +80,7 @@ internal class PnpmDependencyHandler( private val Dependency.workingDir: File get() = File(path) -private val Dependency.packageJsonFile: File get() = workingDir.resolve(NodePackageManager.DEFINITION_FILE) +private val Dependency.packageJsonFile: File get() = workingDir.resolve(NodePackageManagerType.DEFINITION_FILE) /** * pnpm install skips optional dependencies which are not compatible with the environment. In this case the path diff --git a/plugins/package-managers/node/src/main/kotlin/yarn/NpmModuleInfo.kt b/plugins/package-managers/node/src/main/kotlin/yarn/NpmModuleInfo.kt index aa00bc36ca1fa..75fac495f3426 100644 --- a/plugins/package-managers/node/src/main/kotlin/yarn/NpmModuleInfo.kt +++ b/plugins/package-managers/node/src/main/kotlin/yarn/NpmModuleInfo.kt @@ -32,7 +32,7 @@ import org.ossreviewtoolkit.model.Project * contain all the information required to identify a module, construct a [Package] from it, and traverse its * dependency tree. */ -internal data class NpmModuleInfo( +data class NpmModuleInfo( /** The identifier for the represented module. */ val id: Identifier, diff --git a/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt b/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt index aa43ef28bc213..7df5e0781dc56 100644 --- a/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt +++ b/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt @@ -36,8 +36,6 @@ import org.apache.logging.log4j.kotlin.logger import org.apache.logging.log4j.kotlin.loggerOf import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory -import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.analyzer.PackageManagerResult import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.Issue import org.ossreviewtoolkit.model.Project @@ -48,10 +46,9 @@ import org.ossreviewtoolkit.model.createAndLogIssue import org.ossreviewtoolkit.model.readTree import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson -import org.ossreviewtoolkit.plugins.packagemanagers.node.parseProject import org.ossreviewtoolkit.plugins.packagemanagers.node.splitNamespaceAndName import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.DiskCache @@ -98,9 +95,9 @@ open class Yarn( analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration -) : PackageManager(name, "Yarn", analysisRoot, analyzerConfig, repoConfig) { +) : NodePackageManager(name, NodePackageManagerType.YARN, analysisRoot, analyzerConfig, repoConfig) { class Factory : AbstractPackageManagerFactory("Yarn") { - override val globsForDefinitionFiles = listOf(NodePackageManager.DEFINITION_FILE) + override val globsForDefinitionFiles = listOf(NodePackageManagerType.DEFINITION_FILE) override fun create( analysisRoot: File, @@ -114,9 +111,7 @@ open class Yarn( private val rawModuleInfoCache = mutableMapOf>, RawModuleInfo>() - private val graphBuilder by lazy { DependencyGraphBuilder(YarnDependencyHandler(this)) } - - protected fun hasLockfile(projectDir: File) = NodePackageManager.YARN.hasLockfile(projectDir) + override val graphBuilder by lazy { DependencyGraphBuilder(YarnDependencyHandler(this)) } /** * Load the submodule directories of the project defined in [moduleDir]. @@ -136,9 +131,6 @@ open class Yarn( } } - override fun mapDefinitionFiles(definitionFiles: List) = - NpmDetection(definitionFiles).filterApplicable(NodePackageManager.YARN) - override fun beforeResolution(definitionFiles: List) = // We do not actually depend on any features specific to a Yarn version, but we still want to stick to a // fixed minor version to be sure to get consistent results. @@ -179,8 +171,8 @@ open class Yarn( val issues = mutableListOf() val project = runCatching { - val packageJsonFile = projectDir.resolve(NodePackageManager.DEFINITION_FILE) - parseProject(packageJsonFile, analysisRoot, managerName) + val packageJsonFile = projectDir.resolve(NodePackageManagerType.DEFINITION_FILE) + parseProject(packageJsonFile, analysisRoot) }.getOrElse { issues += createAndLogIssue( source = managerName, @@ -302,7 +294,7 @@ open class Yarn( private fun parsePackageJson(moduleDir: File, scopes: Set): RawModuleInfo = rawModuleInfoCache.getOrPut(moduleDir to scopes) { - val packageJsonFile = moduleDir.resolve(NodePackageManager.DEFINITION_FILE) + val packageJsonFile = moduleDir.resolve(NodePackageManagerType.DEFINITION_FILE) logger.debug { "Parsing module info from '${packageJsonFile.absolutePath}'." } val json = packageJsonFile.readTree() @@ -341,11 +333,8 @@ open class Yarn( loadWorkspaceSubmodules(moduleDir) } - override fun createPackageManagerResult(projectResults: Map>) = - PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) - private fun installDependencies(workingDir: File) { - requireLockfile(workingDir) { hasLockfile(workingDir) } + requireLockfile(workingDir) { managerType.hasLockfile(workingDir) } YarnCommand.run(workingDir, "install", "--ignore-scripts", "--ignore-engines", "--immutable").requireSuccess() } diff --git a/plugins/package-managers/node/src/main/kotlin/yarn2/Yarn2.kt b/plugins/package-managers/node/src/main/kotlin/yarn2/Yarn2.kt index 5dff61ec27267..2bd2f216ccf09 100644 --- a/plugins/package-managers/node/src/main/kotlin/yarn2/Yarn2.kt +++ b/plugins/package-managers/node/src/main/kotlin/yarn2/Yarn2.kt @@ -32,8 +32,6 @@ import kotlinx.coroutines.awaitAll import org.apache.logging.log4j.kotlin.logger import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory -import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.analyzer.PackageManagerResult import org.ossreviewtoolkit.analyzer.parseAuthorString import org.ossreviewtoolkit.downloader.VcsHost import org.ossreviewtoolkit.downloader.VersionControlSystem @@ -54,7 +52,7 @@ import org.ossreviewtoolkit.model.createAndLogIssue import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder import org.ossreviewtoolkit.model.utils.DependencyHandler import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.fixDownloadUrl import org.ossreviewtoolkit.plugins.packagemanagers.node.mapLicenses @@ -108,7 +106,7 @@ class Yarn2( analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration -) : PackageManager(name, "Yarn2", analysisRoot, analyzerConfig, repoConfig), CommandLineTool { +) : NodePackageManager(name, NodePackageManagerType.YARN2, analysisRoot, analyzerConfig, repoConfig), CommandLineTool { companion object { /** * The name of the option to disable HTTPS server certificate verification. @@ -122,7 +120,7 @@ class Yarn2( } class Factory : AbstractPackageManagerFactory("Yarn2") { - override val globsForDefinitionFiles = listOf(NodePackageManager.DEFINITION_FILE) + override val globsForDefinitionFiles = listOf(NodePackageManagerType.DEFINITION_FILE) override fun create( analysisRoot: File, @@ -143,9 +141,6 @@ class Yarn2( private val disableRegistryCertificateVerification = options[OPTION_DISABLE_REGISTRY_CERTIFICATE_VERIFICATION].toBoolean() - // A builder to build the dependency graph of the project. - private val graphBuilder = DependencyGraphBuilder(Yarn2DependencyHandler()) - // All the packages parsed by this package manager, mapped by their ids. private val allPackages = mutableMapOf() @@ -155,6 +150,9 @@ class Yarn2( // The issues that have been found when resolving the dependencies. private val issues = mutableListOf() + // A builder to build the dependency graph of the project. + override val graphBuilder = DependencyGraphBuilder(Yarn2DependencyHandler()) + override fun command(workingDir: File?): String { if (workingDir == null) return "" if (isCorepackEnabled(workingDir)) return "yarn" @@ -179,9 +177,6 @@ class Yarn2( isCorepackEnabledInManifest(workingDir) } - override fun mapDefinitionFiles(definitionFiles: List) = - NpmDetection(definitionFiles).filterApplicable(NodePackageManager.YARN2) - override fun beforeResolution(definitionFiles: List) = // We depend on a version >= 2, so we check the version for safety. definitionFiles.forEach { checkVersion(it.parentFile) } @@ -591,15 +586,12 @@ class Yarn2( } } }.toList() - - override fun createPackageManagerResult(projectResults: Map>) = - PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) } /** * A data class storing information about a specific Yarn 2+ module and its dependencies. */ -private data class YarnModuleInfo( +data class YarnModuleInfo( /** The identifier for the represented module. */ val id: Identifier, @@ -709,7 +701,7 @@ private fun getYarnExecutable(workingDir: File): File { */ private fun isCorepackEnabledInManifest(workingDir: File): Boolean = runCatching { - val packageJson = parsePackageJson(workingDir.resolve(NodePackageManager.DEFINITION_FILE)) + val packageJson = parsePackageJson(workingDir.resolve(NodePackageManagerType.DEFINITION_FILE)) !packageJson.packageManager.isNullOrEmpty() }.getOrDefault(false) diff --git a/plugins/package-managers/node/src/test/kotlin/NpmDetectionTest.kt b/plugins/package-managers/node/src/test/kotlin/NpmDetectionTest.kt index eca138f3cb903..475213f0cbee5 100644 --- a/plugins/package-managers/node/src/test/kotlin/NpmDetectionTest.kt +++ b/plugins/package-managers/node/src/test/kotlin/NpmDetectionTest.kt @@ -30,17 +30,17 @@ import io.kotest.matchers.should import io.kotest.matchers.shouldBe import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager.NPM -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager.PNPM -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager.YARN -import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManager.YARN2 +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType.NPM +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType.PNPM +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType.YARN +import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType.YARN2 import org.ossreviewtoolkit.utils.common.withoutPrefix import org.ossreviewtoolkit.utils.test.getAssetFile class NpmDetectionTest : WordSpec({ "All Node package manager detections" should { "ignore empty lockfiles" { - NodePackageManager.entries.forAll { + NodePackageManagerType.entries.forAll { val lockfile = tempdir().resolve(it.lockfileName).apply { writeText("") } @@ -50,7 +50,7 @@ class NpmDetectionTest : WordSpec({ } "ignore empty workspace files" { - NodePackageManager.entries.forAll { + NodePackageManagerType.entries.forAll { val workspaceFile = tempdir().resolve(it.workspaceFileName).apply { writeText("") } @@ -66,7 +66,8 @@ class NpmDetectionTest : WordSpec({ resolve("package.json").writeText("{}") } - NodePackageManager.forDirectory(projectDir) shouldContainExactlyInAnyOrder NodePackageManager.entries + NodePackageManagerType.forDirectory(projectDir) shouldContainExactlyInAnyOrder + NodePackageManagerType.entries } "return only those managers whose lockfiles are present" { @@ -76,7 +77,7 @@ class NpmDetectionTest : WordSpec({ resolve(PNPM.lockfileName).writeText("#") } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM, PNPM) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM, PNPM) } "return only NPM if distinguished by lockfile" { @@ -85,7 +86,7 @@ class NpmDetectionTest : WordSpec({ resolve(NPM.lockfileName).writeText("{}") } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM) } "return only NPM if distinguished by other file" { @@ -94,7 +95,7 @@ class NpmDetectionTest : WordSpec({ NPM.markerFileName?.also { resolve(it).writeText("{}") } } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(NPM) } "return only PNPM if distinguished by lockfile" { @@ -103,7 +104,7 @@ class NpmDetectionTest : WordSpec({ resolve(PNPM.lockfileName).writeText("#") } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(PNPM) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(PNPM) } "return only PNPM if distinguished by workspace file" { @@ -112,7 +113,7 @@ class NpmDetectionTest : WordSpec({ resolve(PNPM.workspaceFileName).writeText("#") } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(PNPM) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(PNPM) } "return only YARN if distinguished by lockfile" { @@ -121,7 +122,7 @@ class NpmDetectionTest : WordSpec({ resolve(YARN.lockfileName).writeText(YARN_LOCK_FILE_HEADER) } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(YARN) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(YARN) } "return only YARN2 if distinguished by lockfile" { @@ -130,7 +131,7 @@ class NpmDetectionTest : WordSpec({ resolve(YARN2.lockfileName).writeText(YARN2_LOCK_FILE_HEADER) } - NodePackageManager.forDirectory(projectDir).shouldContainExactlyInAnyOrder(YARN2) + NodePackageManagerType.forDirectory(projectDir).shouldContainExactlyInAnyOrder(YARN2) } } @@ -156,7 +157,7 @@ class NpmDetectionTest : WordSpec({ val projectDir = getAssetFile("projects/synthetic") val definitionFiles = PackageManager.findManagedFiles(projectDir).values.flatten().toSet() - val filteredFiles = NpmDetection(definitionFiles).filterApplicable(NPM) + val filteredFiles = NodePackageManagerDetection(definitionFiles).filterApplicable(NPM) filteredFiles.map { it.relativeTo(projectDir).invariantSeparatorsPath }.shouldContainExactlyInAnyOrder( "npm/no-lockfile/package.json", @@ -193,7 +194,7 @@ class NpmDetectionTest : WordSpec({ val projectDir = getAssetFile("projects/synthetic") val definitionFiles = PackageManager.findManagedFiles(projectDir).values.flatten().toSet() - val filteredFiles = NpmDetection(definitionFiles).filterApplicable(PNPM) + val filteredFiles = NodePackageManagerDetection(definitionFiles).filterApplicable(PNPM) filteredFiles.map { it.relativeTo(projectDir).invariantSeparatorsPath }.shouldContainExactlyInAnyOrder( "pnpm/babel/package.json", @@ -225,7 +226,7 @@ class NpmDetectionTest : WordSpec({ val projectDir = getAssetFile("projects/synthetic") val definitionFiles = PackageManager.findManagedFiles(projectDir).values.flatten().toSet() - val filteredFiles = NpmDetection(definitionFiles).filterApplicable(YARN) + val filteredFiles = NodePackageManagerDetection(definitionFiles).filterApplicable(YARN) filteredFiles.map { it.relativeTo(projectDir).invariantSeparatorsPath }.shouldContainExactlyInAnyOrder( "yarn/babel/package.json", @@ -256,7 +257,7 @@ class NpmDetectionTest : WordSpec({ val projectDir = getAssetFile("projects/synthetic") val definitionFiles = PackageManager.findManagedFiles(projectDir).values.flatten().toSet() - val filteredFiles = NpmDetection(definitionFiles).filterApplicable(YARN2) + val filteredFiles = NodePackageManagerDetection(definitionFiles).filterApplicable(YARN2) filteredFiles.map { it.relativeTo(projectDir).invariantSeparatorsPath }.shouldContainExactlyInAnyOrder( "yarn2/project-with-lockfile/package.json",