diff --git a/plugins/package-managers/node/src/funTest/kotlin/NpmFunTest.kt b/plugins/package-managers/node/src/funTest/kotlin/npm/NpmFunTest.kt similarity index 99% rename from plugins/package-managers/node/src/funTest/kotlin/NpmFunTest.kt rename to plugins/package-managers/node/src/funTest/kotlin/npm/NpmFunTest.kt index 026e28e9edc4..2d4c83de1f8f 100644 --- a/plugins/package-managers/node/src/funTest/kotlin/NpmFunTest.kt +++ b/plugins/package-managers/node/src/funTest/kotlin/npm/NpmFunTest.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.plugins.packagemanagers.node +package org.ossreviewtoolkit.plugins.packagemanagers.node.npm import io.kotest.core.spec.style.WordSpec import io.kotest.engine.spec.tempdir diff --git a/plugins/package-managers/node/src/funTest/kotlin/YarnFunTest.kt b/plugins/package-managers/node/src/funTest/kotlin/yarn/YarnFunTest.kt similarity index 97% rename from plugins/package-managers/node/src/funTest/kotlin/YarnFunTest.kt rename to plugins/package-managers/node/src/funTest/kotlin/yarn/YarnFunTest.kt index def9dcc5fcb4..8449c84c533c 100644 --- a/plugins/package-managers/node/src/funTest/kotlin/YarnFunTest.kt +++ b/plugins/package-managers/node/src/funTest/kotlin/yarn/YarnFunTest.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.plugins.packagemanagers.node +package org.ossreviewtoolkit.plugins.packagemanagers.node.yarn import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.should diff --git a/plugins/package-managers/node/src/main/kotlin/Yarn.kt b/plugins/package-managers/node/src/main/kotlin/Yarn.kt deleted file mode 100644 index 7bb8bd5f0843..000000000000 --- a/plugins/package-managers/node/src/main/kotlin/Yarn.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2017 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 java.lang.invoke.MethodHandles - -import kotlin.time.Duration.Companion.days - -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.decodeToSequence -import kotlinx.serialization.json.jsonPrimitive - -import org.apache.logging.log4j.kotlin.loggerOf - -import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory -import org.ossreviewtoolkit.model.config.AnalyzerConfiguration -import org.ossreviewtoolkit.model.config.RepositoryConfiguration -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmDetection -import org.ossreviewtoolkit.utils.common.DiskCache -import org.ossreviewtoolkit.utils.common.Os -import org.ossreviewtoolkit.utils.common.alsoIfNull -import org.ossreviewtoolkit.utils.common.mebibytes -import org.ossreviewtoolkit.utils.ort.ortDataDirectory - -import org.semver4j.RangesList -import org.semver4j.RangesListFactory - -private val yarnInfoCache = DiskCache( - directory = ortDataDirectory.resolve("cache/analyzer/yarn/info"), - maxCacheSizeInBytes = 100.mebibytes, - maxCacheEntryAgeInSeconds = 7.days.inWholeSeconds -) - -/** - * The [Yarn](https://classic.yarnpkg.com/) package manager for JavaScript. - */ -class Yarn( - name: String, - analysisRoot: File, - analyzerConfig: AnalyzerConfiguration, - repoConfig: RepositoryConfiguration -) : Npm(name, analysisRoot, analyzerConfig, repoConfig) { - class Factory : AbstractPackageManagerFactory("Yarn") { - override val globsForDefinitionFiles = listOf("package.json") - - override fun create( - analysisRoot: File, - analyzerConfig: AnalyzerConfiguration, - repoConfig: RepositoryConfiguration - ) = Yarn(type, analysisRoot, analyzerConfig, repoConfig) - } - - override fun hasLockfile(projectDir: File) = NodePackageManager.YARN.hasLockfile(projectDir) - - override fun command(workingDir: File?) = if (Os.isWindows) "yarn.cmd" else "yarn" - - override fun getVersionRequirement(): RangesList = RangesListFactory.create("1.3.* - 1.22.*") - - 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. - checkVersion() - - override fun runInstall(workingDir: File) = - run(workingDir, "install", "--ignore-scripts", "--ignore-engines", "--immutable") - - override fun getRemotePackageDetails(workingDir: File, packageName: String): PackageJson? { - yarnInfoCache.read(packageName)?.let { return parsePackageJson(it) } - - val process = run(workingDir, "info", "--json", packageName) - - return parseYarnInfo(process.stdout, process.stderr)?.also { - yarnInfoCache.write(packageName, Json.encodeToString(it)) - } - } -} - -private val logger = loggerOf(MethodHandles.lookup().lookupClass()) - -/** - * Parse the given [stdout] of a Yarn _info_ command to a [PackageJson]. The output is typically a JSON object with the - * metadata of the package that was queried. However, under certain circumstances, Yarn may return multiple JSON objects - * separated by newlines; for instance, if the operation is retried due to network problems. This function filters for - * the object with the data based on the _type_ field. Result is *null* if no matching object is found or the input is - * not valid JSON. - * - * Note: The mentioned network issue can be reproduced by setting the network timeout to be very short via the command - * line option '--network-timeout'. - */ -internal fun parseYarnInfo(stdout: String, stderr: String): PackageJson? = - extractDataNodes(stdout, "inspect").firstOrNull()?.let(::parsePackageJson).alsoIfNull { - extractDataNodes(stderr, "warning").forEach { - logger.info { "Warning running Yarn info: ${it.jsonPrimitive.content}" } - } - - extractDataNodes(stderr, "error").forEach { - logger.warn { "Error parsing Yarn info: ${it.jsonPrimitive.content}" } - } - } - -private fun extractDataNodes(output: String, type: String): Set = - runCatching { - output.byteInputStream().use { inputStream -> - Json.decodeToSequence(inputStream) - .filter { (it["type"] as? JsonPrimitive)?.content == type } - .mapNotNullTo(mutableSetOf()) { it["data"] } - } - }.getOrDefault(emptySet()) diff --git a/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt b/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt new file mode 100644 index 000000000000..6a5d30173c6b --- /dev/null +++ b/plugins/package-managers/node/src/main/kotlin/npm/Npm.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2017 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 + */ + +@file:Suppress("TooManyFunctions") + +package org.ossreviewtoolkit.plugins.packagemanagers.node.npm + +import java.io.File + +import org.apache.logging.log4j.kotlin.logger + +import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory +import org.ossreviewtoolkit.model.config.AnalyzerConfiguration +import org.ossreviewtoolkit.model.config.PackageManagerConfiguration +import org.ossreviewtoolkit.model.config.RepositoryConfiguration +import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NodePackageManager +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.yarn.Yarn +import org.ossreviewtoolkit.utils.common.Os +import org.ossreviewtoolkit.utils.common.ProcessCapture +import org.ossreviewtoolkit.utils.common.withoutPrefix + +import org.semver4j.RangesList +import org.semver4j.RangesListFactory + +/** + * The [Node package manager](https://www.npmjs.com/) for JavaScript. + * + * This package manager supports the following [options][PackageManagerConfiguration.options]: + * - *legacyPeerDeps*: If true, the "--legacy-peer-deps" flag is passed to NPM to ignore conflicts in peer dependencies + * which are reported since NPM 7. This allows to analyze NPM 6 projects with peer dependency conflicts. For more + * information see the [documentation](https://docs.npmjs.com/cli/v8/commands/npm-install#strict-peer-deps) and the + * [NPM Blog](https://blog.npmjs.org/post/626173315965468672/npm-v7-series-beta-release-and-semver-major). + */ +class Npm( + name: String, + analysisRoot: File, + analyzerConfig: AnalyzerConfiguration, + repoConfig: RepositoryConfiguration +) : Yarn(name, 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("package.json") + + override fun create( + analysisRoot: File, + analyzerConfig: AnalyzerConfiguration, + repoConfig: RepositoryConfiguration + ) = Npm(type, analysisRoot, analyzerConfig, repoConfig) + } + + private val legacyPeerDeps = options[OPTION_LEGACY_PEER_DEPS].toBoolean() + + private val npmViewCache = mutableMapOf() + + override fun hasLockfile(projectDir: File) = NodePackageManager.NPM.hasLockfile(projectDir) + + override fun command(workingDir: File?) = if (Os.isWindows) "npm.cmd" else "npm" + + override fun getVersionRequirement(): RangesList = RangesListFactory.create("6.* - 10.*") + + 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. + checkVersion() + } + + override fun getRemotePackageDetails(workingDir: File, packageName: String): PackageJson? { + npmViewCache[packageName]?.let { return it } + + return runCatching { + val process = run(workingDir, "info", "--json", packageName) + + org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson(process.stdout) + }.onFailure { e -> + logger.warn { "Error getting details for $packageName in directory $workingDir: ${e.message.orEmpty()}" } + }.onSuccess { + npmViewCache[packageName] = it + }.getOrNull() + } + + override fun runInstall(workingDir: File): ProcessCapture { + val options = listOfNotNull( + "--ignore-scripts", + "--no-audit", + "--legacy-peer-deps".takeIf { legacyPeerDeps } + ) + + val subcommand = if (hasLockfile(workingDir)) "ci" else "install" + return ProcessCapture(workingDir, command(workingDir), subcommand, *options.toTypedArray()) + } +} + +internal fun List.groupLines(vararg markers: String): List { + val ignorableLinePrefixes = setOf("code ", "errno ", "path ", "syscall ") + val singleLinePrefixes = setOf("deprecated ", "skipping integrity check for git dependency ") + val minCommonPrefixLength = 5 + + val issueLines = mapNotNull { line -> + markers.firstNotNullOfOrNull { marker -> + line.withoutPrefix(marker)?.takeUnless { ignorableLinePrefixes.any { prefix -> it.startsWith(prefix) } } + } + } + + var commonPrefix: String + var previousPrefix = "" + + val collapsedLines = issueLines.distinct().fold(mutableListOf()) { messages, line -> + if (messages.isEmpty()) { + // The first line is always added including the prefix. The prefix will be removed later. + messages += line + } else { + // Find the longest common prefix that ends with space. + commonPrefix = line.commonPrefixWith(messages.last()) + if (!commonPrefix.endsWith(' ')) { + // Deal with prefixes being used on their own as separators. + commonPrefix = if ("$commonPrefix " == previousPrefix || line.startsWith("$commonPrefix ")) { + "$commonPrefix " + } else { + commonPrefix.dropLastWhile { it != ' ' } + } + } + + if (commonPrefix !in singleLinePrefixes && commonPrefix.length >= minCommonPrefixLength) { + // Do not drop the whole prefix but keep the space when concatenating lines. + messages[messages.size - 1] += line.drop(commonPrefix.length - 1).trimEnd() + previousPrefix = commonPrefix + } else { + // Remove the prefix from previously added message start. + messages[messages.size - 1] = messages.last().removePrefix(previousPrefix).trimStart() + messages += line + } + } + + messages + } + + if (collapsedLines.isNotEmpty()) { + // Remove the prefix from the last added message start. + collapsedLines[collapsedLines.size - 1] = collapsedLines.last().removePrefix(previousPrefix).trimStart() + } + + val nonFooterLines = collapsedLines.takeWhile { + // Skip any footer as a whole. + it != "A complete log of this run can be found in:" + } + + // If no lines but the last end with a dot, assume the message to be a single sentence. + return if ( + nonFooterLines.size > 1 && + nonFooterLines.last().endsWith('.') && + nonFooterLines.subList(0, nonFooterLines.size - 1).none { it.endsWith('.') } + ) { + listOf(nonFooterLines.joinToString(" ")) + } else { + nonFooterLines.map { it.trim() } + } +} 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 62c703525a85..90a3a88109c9 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/Pnpm.kt @@ -33,9 +33,9 @@ import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder 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.utils.NodePackageManager import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmDetection +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parseProject import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.stashDirectories 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 edeff23ef756..a3b0f29fa12d 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/PnpmDependencyHandler.kt @@ -27,9 +27,9 @@ import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.utils.DependencyHandler import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson -import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackage import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.pnpm.ModuleInfo.Dependency +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parsePackage import org.ossreviewtoolkit.utils.common.realFile internal class PnpmDependencyHandler(private val pnpm: Pnpm) : DependencyHandler { diff --git a/plugins/package-managers/node/src/main/kotlin/utils/NpmSupport.kt b/plugins/package-managers/node/src/main/kotlin/utils/NpmSupport.kt index b39d9527fe77..1f3766e3ff38 100644 --- a/plugins/package-managers/node/src/main/kotlin/utils/NpmSupport.kt +++ b/plugins/package-managers/node/src/main/kotlin/utils/NpmSupport.kt @@ -19,10 +19,27 @@ package org.ossreviewtoolkit.plugins.packagemanagers.node.utils +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.plugins.packagemanagers.node.PackageJson +import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson +import org.ossreviewtoolkit.utils.common.realFile import org.ossreviewtoolkit.utils.common.toUri import org.ossreviewtoolkit.utils.spdx.SpdxConstants @@ -137,6 +154,142 @@ internal fun parseNpmVcsInfo(packageJson: PackageJson): VcsInfo { ) } +/** + * Construct a [Package] by parsing its _package.json_ file and - if applicable - querying additional + * content via the `npm view` command. The result is a [Pair] with the raw identifier and the new package. + */ +internal fun parsePackage( + workingDir: File, + packageJsonFile: File, + getRemotePackageDetails: (workingDir: File, packageName: String) -> PackageJson? +): Package { + val packageJson = parsePackageJson(packageJsonFile) + + // The "name" and "version" fields are only required if the package is going to be published, otherwise they are + // optional, see + // - https://docs.npmjs.com/cli/v10/configuring-npm/package-json#name + // - https://docs.npmjs.com/cli/v10/configuring-npm/package-json#version + // So, projects analyzed by ORT might not have these fields set. + val rawName = packageJson.name.orEmpty() // TODO: Fall back to a generated name if the name is unset. + val (namespace, name) = splitNpmNamespaceAndName(rawName) + val version = packageJson.version ?: NON_EXISTING_SEMVER + + val declaredLicenses = packageJson.licenses.mapNpmLicenses() + val authors = parseNpmAuthor(packageJson.authors.firstOrNull()) // TODO: parse all authors. + + var description = packageJson.description.orEmpty() + var homepageUrl = packageJson.homepage.orEmpty() + + // Note that all fields prefixed with "_" are considered private to NPM and should not be relied on. + var downloadUrl = expandNpmShortcutUrl(packageJson.resolved.orEmpty()).ifEmpty { + // If the normalized form of the specified dependency contains a URL as the version, expand and use it. + val fromVersion = packageJson.from.orEmpty().substringAfterLast('@') + expandNpmShortcutUrl(fromVersion).takeIf { it != fromVersion }.orEmpty() + } + + var hash = Hash.create(packageJson.integrity.orEmpty()) + + var vcsFromPackage = parseNpmVcsInfo(packageJson) + + val id = Identifier("NPM", namespace, name, version) + + val hasIncompleteData = description.isEmpty() || homepageUrl.isEmpty() || downloadUrl.isEmpty() + || hash == Hash.NONE || vcsFromPackage == VcsInfo.EMPTY + + if (hasIncompleteData) { + getRemotePackageDetails(workingDir, "$rawName@$version")?.let { details -> + if (description.isEmpty()) description = details.description.orEmpty() + if (homepageUrl.isEmpty()) homepageUrl = details.homepage.orEmpty() + + details.dist?.let { dist -> + if (downloadUrl.isEmpty() || hash == Hash.NONE) { + downloadUrl = dist.tarball.orEmpty() + hash = Hash.create(dist.shasum.orEmpty()) + } + } + + // Do not replace but merge, because it happens that `package.json` has VCS info while + // `npm view` doesn't, for example for dependencies hosted on GitLab package registry. + vcsFromPackage = vcsFromPackage.merge(parseNpmVcsInfo(details)) + } + } + + downloadUrl = downloadUrl.fixNpmDownloadUrl() + + val vcsFromDownloadUrl = VcsHost.parseUrl(downloadUrl) + if (vcsFromDownloadUrl.url != downloadUrl) { + vcsFromPackage = vcsFromPackage.merge(vcsFromDownloadUrl) + } + + val module = Package( + id = id, + authors = authors, + declaredLicenses = declaredLicenses, + description = description, + homepageUrl = homepageUrl, + binaryArtifact = RemoteArtifact.EMPTY, + sourceArtifact = RemoteArtifact( + url = VcsHost.toArchiveDownloadUrl(vcsFromDownloadUrl) ?: downloadUrl, + hash = hash + ), + vcs = vcsFromPackage, + vcsProcessed = processPackageVcs(vcsFromPackage, homepageUrl) + ) + + require(module.id.name.isNotEmpty()) { + "Generated package info for '${id.toCoordinates()}' has no name." + } + + require(module.id.version.isNotEmpty()) { + "Generated package info for '${id.toCoordinates()}' has no version." + } + + return module +} + +private val logger = loggerOf(MethodHandles.lookup().lookupClass()) + +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) = splitNpmNamespaceAndName(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.mapNpmLicenses() + val authors = parseNpmAuthor(packageJson.authors.firstOrNull()) // TODO: parse all authors. + val homepageUrl = packageJson.homepage.orEmpty() + val projectDir = packageJsonFile.parentFile.realFile() + val vcsFromPackage = parseNpmVcsInfo(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), + 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.kt b/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt similarity index 56% rename from plugins/package-managers/node/src/main/kotlin/Npm.kt rename to plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt index 2590e397a123..bd1be49ff8e1 100644 --- a/plugins/package-managers/node/src/main/kotlin/Npm.kt +++ b/plugins/package-managers/node/src/main/kotlin/yarn/Yarn.kt @@ -17,64 +17,69 @@ * License-Filename: LICENSE */ -@file:Suppress("TooManyFunctions") - -package org.ossreviewtoolkit.plugins.packagemanagers.node +package org.ossreviewtoolkit.plugins.packagemanagers.node.yarn import java.io.File +import java.lang.invoke.MethodHandles import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.days + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeToSequence +import kotlinx.serialization.json.jsonPrimitive + 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.PackageManager.Companion.getFallbackProjectName -import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processPackageVcs -import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processProjectVcs import org.ossreviewtoolkit.analyzer.PackageManagerResult -import org.ossreviewtoolkit.downloader.VcsHost -import org.ossreviewtoolkit.downloader.VersionControlSystem import org.ossreviewtoolkit.model.DependencyGraph -import org.ossreviewtoolkit.model.Hash import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.Issue -import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.Project import org.ossreviewtoolkit.model.ProjectAnalyzerResult -import org.ossreviewtoolkit.model.RemoteArtifact import org.ossreviewtoolkit.model.Severity -import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.config.AnalyzerConfiguration -import org.ossreviewtoolkit.model.config.PackageManagerConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.createAndLogIssue import org.ossreviewtoolkit.model.readTree import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NON_EXISTING_SEMVER +import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson +import org.ossreviewtoolkit.plugins.packagemanagers.node.npm.groupLines +import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackageJson import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NodePackageManager -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmDependencyHandler import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmDetection -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.NpmModuleInfo -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.expandNpmShortcutUrl -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.fixNpmDownloadUrl -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.mapNpmLicenses -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parseNpmAuthor -import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parseNpmVcsInfo +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parseProject import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.splitNpmNamespaceAndName import org.ossreviewtoolkit.utils.common.CommandLineTool +import org.ossreviewtoolkit.utils.common.DiskCache import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.ProcessCapture +import org.ossreviewtoolkit.utils.common.alsoIfNull import org.ossreviewtoolkit.utils.common.collectMessages import org.ossreviewtoolkit.utils.common.fieldNamesOrEmpty import org.ossreviewtoolkit.utils.common.isSymbolicLink +import org.ossreviewtoolkit.utils.common.mebibytes import org.ossreviewtoolkit.utils.common.realFile import org.ossreviewtoolkit.utils.common.stashDirectories import org.ossreviewtoolkit.utils.common.textValueOrEmpty -import org.ossreviewtoolkit.utils.common.withoutPrefix +import org.ossreviewtoolkit.utils.ort.ortDataDirectory import org.semver4j.RangesList import org.semver4j.RangesListFactory +private val yarnInfoCache = DiskCache( + directory = ortDataDirectory.resolve("cache/analyzer/yarn/info"), + maxCacheSizeInBytes = 100.mebibytes, + maxCacheEntryAgeInSeconds = 7.days.inWholeSeconds +) + /** Name of the scope with the regular dependencies. */ private const val DEPENDENCIES_SCOPE = "dependencies" @@ -85,47 +90,37 @@ private const val OPTIONAL_DEPENDENCIES_SCOPE = "optionalDependencies" private const val DEV_DEPENDENCIES_SCOPE = "devDependencies" /** - * The [Node package manager](https://www.npmjs.com/) for JavaScript. - * - * This package manager supports the following [options][PackageManagerConfiguration.options]: - * - *legacyPeerDeps*: If true, the "--legacy-peer-deps" flag is passed to NPM to ignore conflicts in peer dependencies - * which are reported since NPM 7. This allows to analyze NPM 6 projects with peer dependency conflicts. For more - * information see the [documentation](https://docs.npmjs.com/cli/v8/commands/npm-install#strict-peer-deps) and the - * [NPM Blog](https://blog.npmjs.org/post/626173315965468672/npm-v7-series-beta-release-and-semver-major). + * The [Yarn](https://classic.yarnpkg.com/) package manager for JavaScript. */ -open class Npm( +open class Yarn( name: String, analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration ) : PackageManager(name, analysisRoot, analyzerConfig, repoConfig), CommandLineTool { - companion object { - /** Name of the configuration option to toggle legacy peer dependency support. */ - const val OPTION_LEGACY_PEER_DEPS = "legacyPeerDeps" - } - - class Factory : AbstractPackageManagerFactory("NPM") { + class Factory : AbstractPackageManagerFactory("Yarn") { override val globsForDefinitionFiles = listOf("package.json") override fun create( analysisRoot: File, analyzerConfig: AnalyzerConfiguration, repoConfig: RepositoryConfiguration - ) = Npm(type, analysisRoot, analyzerConfig, repoConfig) + ) = Yarn(type, analysisRoot, analyzerConfig, repoConfig) } - private val legacyPeerDeps = options[OPTION_LEGACY_PEER_DEPS].toBoolean() + /** Cache for submodules identified by its moduleDir absolutePath */ + private val submodulesCache = ConcurrentHashMap>() - private val graphBuilder by lazy { DependencyGraphBuilder(NpmDependencyHandler(this)) } + private val rawModuleInfoCache = mutableMapOf>, RawModuleInfo>() - private val npmViewCache = mutableMapOf() + private val graphBuilder by lazy { DependencyGraphBuilder(YarnDependencyHandler(this)) } - protected open fun hasLockfile(projectDir: File) = NodePackageManager.NPM.hasLockfile(projectDir) + protected open fun hasLockfile(projectDir: File) = NodePackageManager.YARN.hasLockfile(projectDir) /** * Load the submodule directories of the project defined in [moduleDir]. */ - protected open fun loadWorkspaceSubmodules(moduleDir: File): Set { + private fun loadWorkspaceSubmodules(moduleDir: File): Set { val nodeModulesDir = moduleDir.resolve("node_modules") if (!nodeModulesDir.isDirectory) return emptySet() @@ -140,21 +135,17 @@ open class Npm( } } - override fun command(workingDir: File?) = if (Os.isWindows) "npm.cmd" else "npm" + override fun command(workingDir: File?) = if (Os.isWindows) "yarn.cmd" else "yarn" - override fun getVersionRequirement(): RangesList = RangesListFactory.create("6.* - 10.*") + override fun getVersionRequirement(): RangesList = RangesListFactory.create("1.3.* - 1.22.*") override fun mapDefinitionFiles(definitionFiles: List) = - NpmDetection(definitionFiles).filterApplicable(NodePackageManager.NPM) + NpmDetection(definitionFiles).filterApplicable(NodePackageManager.YARN) - 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 + 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. checkVersion() - } - - override fun createPackageManagerResult(projectResults: Map>) = - PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) override fun resolveDependencies(definitionFile: File, labels: Map): List { val workingDir = definitionFile.parentFile @@ -168,6 +159,18 @@ open class Npm( } } + /** + * An internally used data class with information about a module retrieved from the module's package.json. This + * information is further processed and eventually converted to an [NpmModuleInfo] object containing everything + * required by the Npm package manager. + */ + private data class RawModuleInfo( + val name: String, + val version: String, + val dependencyNames: Set, + val packageJson: File + ) + // TODO: Add support for bundledDependencies. private fun resolveDependenciesInternal(definitionFile: File): List { val workingDir = definitionFile.parentFile @@ -238,53 +241,6 @@ open class Npm( } } - internal open fun getRemotePackageDetails(workingDir: File, packageName: String): PackageJson? { - npmViewCache[packageName]?.let { return it } - - return runCatching { - val process = run(workingDir, "info", "--json", packageName) - - parsePackageJson(process.stdout) - }.onFailure { e -> - logger.warn { "Error getting details for $packageName in directory $workingDir: ${e.message.orEmpty()}" } - }.onSuccess { - npmViewCache[packageName] = it - }.getOrNull() - } - - /** Cache for submodules identified by its moduleDir absolutePath */ - private val submodulesCache = ConcurrentHashMap>() - - /** - * Find the directories which are defined as submodules of the project within [moduleDir]. - */ - private fun findWorkspaceSubmodules(moduleDir: File): Set = - submodulesCache.getOrPut(moduleDir.absolutePath) { - loadWorkspaceSubmodules(moduleDir) - } - - /** - * Retrieve all the dependencies of [project] from the given [scopes] and add them to the dependency graph under - * the given [targetScope]. Return the target scope name if dependencies are found; *null* otherwise. - */ - private fun buildDependencyGraphForScopes( - project: Project, - workingDir: File, - scopes: Set, - targetScope: String, - projectDirs: Set, - workspaceDir: File? = null - ): String? { - if (excludes.isScopeExcluded(targetScope)) return null - - val qualifiedScopeName = DependencyGraph.qualifyScope(project, targetScope) - val moduleInfo = checkNotNull(getModuleInfo(workingDir, scopes, projectDirs, listOfNotNull(workspaceDir))) - - moduleInfo.dependencies.forEach { graphBuilder.addDependency(qualifiedScopeName, it) } - - return targetScope.takeUnless { moduleInfo.dependencies.isEmpty() } - } - private fun getModuleInfo( moduleDir: File, scopes: Set, @@ -343,18 +299,26 @@ open class Npm( } /** - * An internally used data class with information about a module retrieved from the module's package.json. This - * information is further processed and eventually converted to an [NpmModuleInfo] object containing everything - * required by the Npm package manager. + * Retrieve all the dependencies of [project] from the given [scopes] and add them to the dependency graph under + * the given [targetScope]. Return the target scope name if dependencies are found; *null* otherwise. */ - private data class RawModuleInfo( - val name: String, - val version: String, - val dependencyNames: Set, - val packageJson: File - ) + private fun buildDependencyGraphForScopes( + project: Project, + workingDir: File, + scopes: Set, + targetScope: String, + projectDirs: Set, + workspaceDir: File? = null + ): String? { + if (excludes.isScopeExcluded(targetScope)) return null - private val rawModuleInfoCache = mutableMapOf>, RawModuleInfo>() + val qualifiedScopeName = DependencyGraph.qualifyScope(project, targetScope) + val moduleInfo = checkNotNull(getModuleInfo(workingDir, scopes, projectDirs, listOfNotNull(workspaceDir))) + + moduleInfo.dependencies.forEach { graphBuilder.addDependency(qualifiedScopeName, it) } + + return targetScope.takeUnless { moduleInfo.dependencies.isEmpty() } + } private fun parsePackageJson(moduleDir: File, scopes: Set): RawModuleInfo = rawModuleInfoCache.getOrPut(moduleDir to scopes) { @@ -389,6 +353,17 @@ open class Npm( ) } + /** + * Find the directories which are defined as submodules of the project within [moduleDir]. + */ + private fun findWorkspaceSubmodules(moduleDir: File): Set = + submodulesCache.getOrPut(moduleDir.absolutePath) { + loadWorkspaceSubmodules(moduleDir) + } + + override fun createPackageManagerResult(projectResults: Map>) = + PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages()) + /** * Install dependencies using the given package manager command. */ @@ -415,226 +390,60 @@ open class Npm( return issues } - protected open fun runInstall(workingDir: File): ProcessCapture { - val options = listOfNotNull( - "--ignore-scripts", - "--no-audit", - "--legacy-peer-deps".takeIf { legacyPeerDeps } - ) - - val subcommand = if (hasLockfile(workingDir)) "ci" else "install" - return ProcessCapture(workingDir, command(workingDir), subcommand, *options.toTypedArray()) - } -} - -private fun findDependencyModuleDir(dependencyName: String, searchModuleDirs: List): List { - searchModuleDirs.forEachIndexed { index, moduleDir -> - // Note: resolve() also works for scoped dependencies, e.g. dependencyName = "@x/y" - val dependencyModuleDir = moduleDir.resolve("node_modules/$dependencyName") - if (dependencyModuleDir.isDirectory) { - return listOf(dependencyModuleDir) + searchModuleDirs.subList(index, searchModuleDirs.size) - } - } + protected open fun runInstall(workingDir: File): ProcessCapture = + run(workingDir, "install", "--ignore-scripts", "--ignore-engines", "--immutable") - return emptyList() -} - -internal fun List.groupLines(vararg markers: String): List { - val ignorableLinePrefixes = setOf("code ", "errno ", "path ", "syscall ") - val singleLinePrefixes = setOf("deprecated ", "skipping integrity check for git dependency ") - val minCommonPrefixLength = 5 - - val issueLines = mapNotNull { line -> - markers.firstNotNullOfOrNull { marker -> - line.withoutPrefix(marker)?.takeUnless { ignorableLinePrefixes.any { prefix -> it.startsWith(prefix) } } - } - } + internal open fun getRemotePackageDetails(workingDir: File, packageName: String): PackageJson? { + yarnInfoCache.read(packageName)?.let { return parsePackageJson(it) } - var commonPrefix: String - var previousPrefix = "" - - val collapsedLines = issueLines.distinct().fold(mutableListOf()) { messages, line -> - if (messages.isEmpty()) { - // The first line is always added including the prefix. The prefix will be removed later. - messages += line - } else { - // Find the longest common prefix that ends with space. - commonPrefix = line.commonPrefixWith(messages.last()) - if (!commonPrefix.endsWith(' ')) { - // Deal with prefixes being used on their own as separators. - commonPrefix = if ("$commonPrefix " == previousPrefix || line.startsWith("$commonPrefix ")) { - "$commonPrefix " - } else { - commonPrefix.dropLastWhile { it != ' ' } - } - } + val process = run(workingDir, "info", "--json", packageName) - if (commonPrefix !in singleLinePrefixes && commonPrefix.length >= minCommonPrefixLength) { - // Do not drop the whole prefix but keep the space when concatenating lines. - messages[messages.size - 1] += line.drop(commonPrefix.length - 1).trimEnd() - previousPrefix = commonPrefix - } else { - // Remove the prefix from previously added message start. - messages[messages.size - 1] = messages.last().removePrefix(previousPrefix).trimStart() - messages += line - } + return parseYarnInfo(process.stdout, process.stderr)?.also { + yarnInfoCache.write(packageName, Json.encodeToString(it)) } - - messages - } - - if (collapsedLines.isNotEmpty()) { - // Remove the prefix from the last added message start. - collapsedLines[collapsedLines.size - 1] = collapsedLines.last().removePrefix(previousPrefix).trimStart() - } - - val nonFooterLines = collapsedLines.takeWhile { - // Skip any footer as a whole. - it != "A complete log of this run can be found in:" - } - - // If no lines but the last end with a dot, assume the message to be a single sentence. - return if ( - nonFooterLines.size > 1 && - nonFooterLines.last().endsWith('.') && - nonFooterLines.subList(0, nonFooterLines.size - 1).none { it.endsWith('.') } - ) { - listOf(nonFooterLines.joinToString(" ")) - } else { - nonFooterLines.map { it.trim() } } } +private val logger = loggerOf(MethodHandles.lookup().lookupClass()) + /** - * Construct a [Package] by parsing its _package.json_ file and - if applicable - querying additional - * content via the `npm view` command. The result is a [Pair] with the raw identifier and the new package. + * Parse the given [stdout] of a Yarn _info_ command to a [PackageJson]. The output is typically a JSON object with the + * metadata of the package that was queried. However, under certain circumstances, Yarn may return multiple JSON objects + * separated by newlines; for instance, if the operation is retried due to network problems. This function filters for + * the object with the data based on the _type_ field. Result is *null* if no matching object is found or the input is + * not valid JSON. + * + * Note: The mentioned network issue can be reproduced by setting the network timeout to be very short via the command + * line option '--network-timeout'. */ -internal fun parsePackage( - workingDir: File, - packageJsonFile: File, - getRemotePackageDetails: (workingDir: File, packageName: String) -> PackageJson? -): Package { - val packageJson = parsePackageJson(packageJsonFile) - - // The "name" and "version" fields are only required if the package is going to be published, otherwise they are - // optional, see - // - https://docs.npmjs.com/cli/v10/configuring-npm/package-json#name - // - https://docs.npmjs.com/cli/v10/configuring-npm/package-json#version - // So, projects analyzed by ORT might not have these fields set. - val rawName = packageJson.name.orEmpty() // TODO: Fall back to a generated name if the name is unset. - val (namespace, name) = splitNpmNamespaceAndName(rawName) - val version = packageJson.version ?: NON_EXISTING_SEMVER - - val declaredLicenses = packageJson.licenses.mapNpmLicenses() - val authors = parseNpmAuthor(packageJson.authors.firstOrNull()) // TODO: parse all authors. - - var description = packageJson.description.orEmpty() - var homepageUrl = packageJson.homepage.orEmpty() - - // Note that all fields prefixed with "_" are considered private to NPM and should not be relied on. - var downloadUrl = expandNpmShortcutUrl(packageJson.resolved.orEmpty()).ifEmpty { - // If the normalized form of the specified dependency contains a URL as the version, expand and use it. - val fromVersion = packageJson.from.orEmpty().substringAfterLast('@') - expandNpmShortcutUrl(fromVersion).takeIf { it != fromVersion }.orEmpty() - } - - var hash = Hash.create(packageJson.integrity.orEmpty()) - - var vcsFromPackage = parseNpmVcsInfo(packageJson) - - val id = Identifier("NPM", namespace, name, version) - - val hasIncompleteData = description.isEmpty() || homepageUrl.isEmpty() || downloadUrl.isEmpty() - || hash == Hash.NONE || vcsFromPackage == VcsInfo.EMPTY - - if (hasIncompleteData) { - getRemotePackageDetails(workingDir, "$rawName@$version")?.let { details -> - if (description.isEmpty()) description = details.description.orEmpty() - if (homepageUrl.isEmpty()) homepageUrl = details.homepage.orEmpty() - - details.dist?.let { dist -> - if (downloadUrl.isEmpty() || hash == Hash.NONE) { - downloadUrl = dist.tarball.orEmpty() - hash = Hash.create(dist.shasum.orEmpty()) - } - } - - // Do not replace but merge, because it happens that `package.json` has VCS info while - // `npm view` doesn't, for example for dependencies hosted on GitLab package registry. - vcsFromPackage = vcsFromPackage.merge(parseNpmVcsInfo(details)) +internal fun parseYarnInfo(stdout: String, stderr: String): PackageJson? = + extractDataNodes(stdout, "inspect").firstOrNull()?.let(::parsePackageJson).alsoIfNull { + extractDataNodes(stderr, "warning").forEach { + logger.info { "Warning running Yarn info: ${it.jsonPrimitive.content}" } } - } - - downloadUrl = downloadUrl.fixNpmDownloadUrl() - - val vcsFromDownloadUrl = VcsHost.parseUrl(downloadUrl) - if (vcsFromDownloadUrl.url != downloadUrl) { - vcsFromPackage = vcsFromPackage.merge(vcsFromDownloadUrl) - } - - val module = Package( - id = id, - authors = authors, - declaredLicenses = declaredLicenses, - description = description, - homepageUrl = homepageUrl, - binaryArtifact = RemoteArtifact.EMPTY, - sourceArtifact = RemoteArtifact( - url = VcsHost.toArchiveDownloadUrl(vcsFromDownloadUrl) ?: downloadUrl, - hash = hash - ), - vcs = vcsFromPackage, - vcsProcessed = processPackageVcs(vcsFromPackage, homepageUrl) - ) - require(module.id.name.isNotEmpty()) { - "Generated package info for '${id.toCoordinates()}' has no name." - } - - require(module.id.version.isNotEmpty()) { - "Generated package info for '${id.toCoordinates()}' has no version." + extractDataNodes(stderr, "error").forEach { + logger.warn { "Error parsing Yarn info: ${it.jsonPrimitive.content}" } + } } - return module -} - -internal fun parseProject(packageJsonFile: File, analysisRoot: File, managerName: String): Project { - Npm.logger.debug { "Parsing project info from '$packageJsonFile'." } - - val packageJson = parsePackageJson(packageJsonFile) - - val rawName = packageJson.name.orEmpty() - val (namespace, name) = splitNpmNamespaceAndName(rawName) - - val projectName = name.ifBlank { - getFallbackProjectName(analysisRoot, packageJsonFile).also { - Npm.logger.warn { "'$packageJsonFile' does not define a name, falling back to '$it'." } +private fun extractDataNodes(output: String, type: String): Set = + runCatching { + output.byteInputStream().use { inputStream -> + Json.decodeToSequence(inputStream) + .filter { (it["type"] as? JsonPrimitive)?.content == type } + .mapNotNullTo(mutableSetOf()) { it["data"] } } - } + }.getOrDefault(emptySet()) - val version = packageJson.version.orEmpty() - if (version.isBlank()) { - Npm.logger.warn { "'$packageJsonFile' does not define a version." } +private fun findDependencyModuleDir(dependencyName: String, searchModuleDirs: List): List { + searchModuleDirs.forEachIndexed { index, moduleDir -> + // Note: resolve() also works for scoped dependencies, e.g. dependencyName = "@x/y" + val dependencyModuleDir = moduleDir.resolve("node_modules/$dependencyName") + if (dependencyModuleDir.isDirectory) { + return listOf(dependencyModuleDir) + searchModuleDirs.subList(index, searchModuleDirs.size) + } } - val declaredLicenses = packageJson.licenses.mapNpmLicenses() - val authors = parseNpmAuthor(packageJson.authors.firstOrNull()) // TODO: parse all authors. - val homepageUrl = packageJson.homepage.orEmpty() - val projectDir = packageJsonFile.parentFile.realFile() - val vcsFromPackage = parseNpmVcsInfo(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), - homepageUrl = homepageUrl - ) + return emptyList() } diff --git a/plugins/package-managers/node/src/main/kotlin/utils/NpmDependencyHandler.kt b/plugins/package-managers/node/src/main/kotlin/yarn/YarnDependencyHandler.kt similarity index 91% rename from plugins/package-managers/node/src/main/kotlin/utils/NpmDependencyHandler.kt rename to plugins/package-managers/node/src/main/kotlin/yarn/YarnDependencyHandler.kt index 1189052060d4..b40409e27487 100644 --- a/plugins/package-managers/node/src/main/kotlin/utils/NpmDependencyHandler.kt +++ b/plugins/package-managers/node/src/main/kotlin/yarn/YarnDependencyHandler.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.plugins.packagemanagers.node.utils +package org.ossreviewtoolkit.plugins.packagemanagers.node.yarn import java.io.File @@ -27,8 +27,7 @@ import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.Project import org.ossreviewtoolkit.model.utils.DependencyHandler -import org.ossreviewtoolkit.plugins.packagemanagers.node.Npm -import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackage +import org.ossreviewtoolkit.plugins.packagemanagers.node.utils.parsePackage /** * A data class storing information about a specific NPM module and its dependencies. @@ -67,7 +66,7 @@ internal data class NpmModuleInfo( /** * A specialized [DependencyHandler] implementation for NPM. */ -internal class NpmDependencyHandler(private val npm: Npm) : DependencyHandler { +internal class YarnDependencyHandler(private val npm: Yarn) : DependencyHandler { override fun identifierFor(dependency: NpmModuleInfo): Identifier = dependency.id override fun dependenciesFor(dependency: NpmModuleInfo): List = dependency.dependencies.toList() diff --git a/plugins/package-managers/node/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory b/plugins/package-managers/node/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory index 8a4ce674dfd8..5f475614711a 100644 --- a/plugins/package-managers/node/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory +++ b/plugins/package-managers/node/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory @@ -1,4 +1,4 @@ -org.ossreviewtoolkit.plugins.packagemanagers.node.Npm$Factory +org.ossreviewtoolkit.plugins.packagemanagers.node.npm.Npm$Factory org.ossreviewtoolkit.plugins.packagemanagers.node.pnpm.Pnpm$Factory -org.ossreviewtoolkit.plugins.packagemanagers.node.Yarn$Factory +org.ossreviewtoolkit.plugins.packagemanagers.node.yarn.Yarn$Factory org.ossreviewtoolkit.plugins.packagemanagers.node.yarn2.Yarn2$Factory diff --git a/plugins/package-managers/node/src/test/kotlin/NpmTest.kt b/plugins/package-managers/node/src/test/kotlin/npm/NpmTest.kt similarity index 98% rename from plugins/package-managers/node/src/test/kotlin/NpmTest.kt rename to plugins/package-managers/node/src/test/kotlin/npm/NpmTest.kt index 78d9bf39daaa..9cba2ab3bb9b 100644 --- a/plugins/package-managers/node/src/test/kotlin/NpmTest.kt +++ b/plugins/package-managers/node/src/test/kotlin/npm/NpmTest.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.plugins.packagemanagers.node +package org.ossreviewtoolkit.plugins.packagemanagers.node.npm import io.kotest.core.spec.style.WordSpec import io.kotest.engine.spec.tempdir diff --git a/plugins/package-managers/node/src/test/kotlin/utils/NpmDependencyHandlerTest.kt b/plugins/package-managers/node/src/test/kotlin/utils/NpmDependencyHandlerTest.kt index d00bea51ed6b..cc13e2aa85a4 100644 --- a/plugins/package-managers/node/src/test/kotlin/utils/NpmDependencyHandlerTest.kt +++ b/plugins/package-managers/node/src/test/kotlin/utils/NpmDependencyHandlerTest.kt @@ -31,7 +31,9 @@ import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.config.AnalyzerConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration -import org.ossreviewtoolkit.plugins.packagemanagers.node.Npm +import org.ossreviewtoolkit.plugins.packagemanagers.node.npm.Npm +import org.ossreviewtoolkit.plugins.packagemanagers.node.yarn.NpmModuleInfo +import org.ossreviewtoolkit.plugins.packagemanagers.node.yarn.YarnDependencyHandler import org.ossreviewtoolkit.utils.test.USER_DIR class NpmDependencyHandlerTest : StringSpec({ @@ -100,7 +102,7 @@ private fun createModuleInfo( ): NpmModuleInfo = NpmModuleInfo(id, packageFile.parentFile, packageFile, dependencies, isProject) /** - * Creates an [NpmDependencyHandler] instance to be used by test cases. + * Creates an [YarnDependencyHandler] instance to be used by test cases. */ private fun createHandler() = - NpmDependencyHandler(Npm("NPM", USER_DIR, AnalyzerConfiguration(), RepositoryConfiguration())) + YarnDependencyHandler(Npm("NPM", USER_DIR, AnalyzerConfiguration(), RepositoryConfiguration())) diff --git a/plugins/package-managers/node/src/test/kotlin/YarnTest.kt b/plugins/package-managers/node/src/test/kotlin/yarn/YarnTest.kt similarity index 97% rename from plugins/package-managers/node/src/test/kotlin/YarnTest.kt rename to plugins/package-managers/node/src/test/kotlin/yarn/YarnTest.kt index defcfd72d1a4..e91cddb25aba 100644 --- a/plugins/package-managers/node/src/test/kotlin/YarnTest.kt +++ b/plugins/package-managers/node/src/test/kotlin/yarn/YarnTest.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.plugins.packagemanagers.node +package org.ossreviewtoolkit.plugins.packagemanagers.node.yarn import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.nulls.beNull