Skip to content

Commit

Permalink
fix(pnpm): Fix parsing of JSON output for nested projects
Browse files Browse the repository at this point in the history
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 oss-review-toolkit#9784.

Signed-off-by: Oliver Heger <[email protected]>
  • Loading branch information
oheger-bosch committed Jan 29, 2025
1 parent c15996b commit 5970619
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModuleInfo> = JSON.decodeFromString(json)
internal fun parsePnpmList(json: String): List<ModuleInfo> =
JSON.decodeToSequence<List<ModuleInfo>>(
ByteArrayInputStream(json.toByteArray()),
DecodeSequenceMode.WHITESPACE_SEPARATED
).toList().flatten()

@Serializable
data class ModuleInfo(
Expand Down
28 changes: 28 additions & 0 deletions plugins/package-managers/node/src/test/assets/pnpm-list.json
Original file line number Diff line number Diff line change
@@ -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/[email protected]/node_modules/eslint-scope",
"path": "/tmp/work/root/node_modules/.pnpm/[email protected]/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/@[email protected]/node_modules/@types/eslint",
"path": "/tmp/work/other_root/node_modules/.pnpm/@[email protected]/node_modules/@types/eslint"
}
}
}
]
24 changes: 24 additions & 0 deletions plugins/package-managers/node/src/test/assets/pnpm-multi-list.json
Original file line number Diff line number Diff line change
@@ -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/[email protected]/node_modules/eslint-scope",
"path": "/tmp/work/top/node_modules/.pnpm/[email protected]/node_modules/eslint-scope"
}
}
}
]

[
{
"name": "nested-project",
"version": "1.0.0",
"path": "/tmp/work/top/nested",
"private": false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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/[email protected]/node_modules/eslint-scope",
path = "/tmp/work/root/node_modules/.pnpm/[email protected]/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/@[email protected]/node_modules/@types/eslint",
path = "/tmp/work/other_root/node_modules/.pnpm/@[email protected]" +
"/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/[email protected]/node_modules/eslint-scope",
path = "/tmp/work/top/node_modules/.pnpm/[email protected]/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
}
}
})

0 comments on commit 5970619

Please sign in to comment.