From 59706199dc87c50ec4c73af72e071a70b77de098 Mon Sep 17 00:00:00 2001 From: Oliver Heger Date: Wed, 29 Jan 2025 12:59:57 +0100 Subject: [PATCH] fix(pnpm): Fix parsing of JSON output for nested projects If PNPM encounters nested projects, the out of the `list` command is not well-formed JSON, but consists of multiple arrays. Change the `parsePnpmList()` function to handle this format correctly. Fixes #9784. Signed-off-by: Oliver Heger --- .../node/src/main/kotlin/pnpm/ModuleInfo.kt | 10 +- .../node/src/test/assets/pnpm-list.json | 28 ++++++ .../node/src/test/assets/pnpm-multi-list.json | 24 +++++ .../src/test/kotlin/pnpm/ModuleInfoTest.kt | 97 +++++++++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 plugins/package-managers/node/src/test/assets/pnpm-list.json create mode 100644 plugins/package-managers/node/src/test/assets/pnpm-multi-list.json create mode 100644 plugins/package-managers/node/src/test/kotlin/pnpm/ModuleInfoTest.kt 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 72c906c1ead08..ed400e430fb62 100644 --- a/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt +++ b/plugins/package-managers/node/src/main/kotlin/pnpm/ModuleInfo.kt @@ -19,12 +19,20 @@ package org.ossreviewtoolkit.plugins.packagemanagers.node.pnpm +import java.io.ByteArrayInputStream + import kotlinx.serialization.Serializable +import kotlinx.serialization.json.DecodeSequenceMode import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeToSequence private val JSON = Json { ignoreUnknownKeys = true } -internal fun parsePnpmList(json: String): List = JSON.decodeFromString(json) +internal fun parsePnpmList(json: String): List = + JSON.decodeToSequence>( + ByteArrayInputStream(json.toByteArray()), + DecodeSequenceMode.WHITESPACE_SEPARATED + ).toList().flatten() @Serializable data class ModuleInfo( diff --git a/plugins/package-managers/node/src/test/assets/pnpm-list.json b/plugins/package-managers/node/src/test/assets/pnpm-list.json new file mode 100644 index 0000000000000..ac74a9a314706 --- /dev/null +++ b/plugins/package-managers/node/src/test/assets/pnpm-list.json @@ -0,0 +1,28 @@ +[ + { + "name": "some-project", + "version": "1.0.0", + "path": "/tmp/work/root", + "private": false, + "dependencies": { + "eslint-scope": { + "from": "eslint-scope", + "version": "link:node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope", + "path": "/tmp/work/root/node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope" + } + } + }, + { + "name": "other-project", + "version": "1.0.1", + "path": "/tmp/work/other_root", + "private": false, + "dependencies": { + "@types/eslint": { + "from": "@types/eslint", + "version": "link:node_modules/.pnpm/@types+eslint@8.56.2/node_modules/@types/eslint", + "path": "/tmp/work/other_root/node_modules/.pnpm/@types+eslint@8.56.2/node_modules/@types/eslint" + } + } + } +] diff --git a/plugins/package-managers/node/src/test/assets/pnpm-multi-list.json b/plugins/package-managers/node/src/test/assets/pnpm-multi-list.json new file mode 100644 index 0000000000000..e48232d69159b --- /dev/null +++ b/plugins/package-managers/node/src/test/assets/pnpm-multi-list.json @@ -0,0 +1,24 @@ +[ + { + "name": "outer-project", + "version": "1.0.0", + "path": "/tmp/work/top", + "private": false, + "dependencies": { + "eslint-scope": { + "from": "eslint-scope", + "version": "link:node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope", + "path": "/tmp/work/top/node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope" + } + } + } +] + +[ + { + "name": "nested-project", + "version": "1.0.0", + "path": "/tmp/work/top/nested", + "private": false + } +] diff --git a/plugins/package-managers/node/src/test/kotlin/pnpm/ModuleInfoTest.kt b/plugins/package-managers/node/src/test/kotlin/pnpm/ModuleInfoTest.kt new file mode 100644 index 0000000000000..fbf3f6961a244 --- /dev/null +++ b/plugins/package-managers/node/src/test/kotlin/pnpm/ModuleInfoTest.kt @@ -0,0 +1,97 @@ +/* + * 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.pnpm + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.collections.shouldContainExactly + +import java.io.File + +class ModuleInfoTest : WordSpec({ + "parsePnpmList" should { + "handle normal PNPM output" { + val input = File("src/test/assets/pnpm-list.json").readText() + + val expectedResults = listOf( + ModuleInfo( + name = "some-project", + version = "1.0.0", + path = "/tmp/work/root", + private = false, + dependencies = mapOf( + "eslint-scope" to ModuleInfo.Dependency( + from = "eslint-scope", + version = "link:node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope", + path = "/tmp/work/root/node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope" + ) + ) + ), + ModuleInfo( + name = "other-project", + version = "1.0.1", + path = "/tmp/work/other_root", + private = false, + dependencies = mapOf( + "@types/eslint" to ModuleInfo.Dependency( + from = "@types/eslint", + version = "link:node_modules/.pnpm/@types+eslint@8.56.2/node_modules/@types/eslint", + path = "/tmp/work/other_root/node_modules/.pnpm/@types+eslint@8.56.2" + + "/node_modules/@types/eslint" + ) + ) + ) + ) + + val moduleInfos = parsePnpmList(input) + + moduleInfos shouldContainExactly expectedResults + } + + "handle multiple JSON arrays" { + val input = File("src/test/assets/pnpm-multi-list.json").readText() + + val expectedResults = listOf( + ModuleInfo( + name = "outer-project", + version = "1.0.0", + path = "/tmp/work/top", + private = false, + dependencies = mapOf( + "eslint-scope" to ModuleInfo.Dependency( + from = "eslint-scope", + version = "link:node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope", + path = "/tmp/work/top/node_modules/.pnpm/eslint-scope@5.1.1/node_modules/eslint-scope" + ) + ) + ), + ModuleInfo( + name = "nested-project", + version = "1.0.0", + path = "/tmp/work/top/nested", + private = false + ) + ) + + val moduleInfos = parsePnpmList(input) + + moduleInfos shouldContainExactly expectedResults + } + } +})