Skip to content

Commit

Permalink
Set pnpm workspace properties recursively (#1587)
Browse files Browse the repository at this point in the history
* Set pnpm workspace properties recursively

Signed-off-by: Prabhu Subramanian <[email protected]>

* Fix tests

Signed-off-by: Prabhu Subramanian <[email protected]>

---------

Signed-off-by: Prabhu Subramanian <[email protected]>
  • Loading branch information
prabhu authored Jan 21, 2025
1 parent 34f2242 commit 459dfe1
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 29 deletions.
4 changes: 2 additions & 2 deletions lib/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2533,9 +2533,9 @@ export async function createNodejsBom(path, options) {
const apkgRef = `pkg:npm/${adep}`;
workspaceDirectDeps[workspaceRef].add(apkgRef);
if (!depsWorkspaceRefs[apkgRef]) {
depsWorkspaceRefs[apkgRef] = new Set();
depsWorkspaceRefs[apkgRef] = [];
}
depsWorkspaceRefs[apkgRef].add(workspaceRef);
depsWorkspaceRefs[apkgRef].push(workspaceRef);
}
}
}
Expand Down
133 changes: 108 additions & 25 deletions lib/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,59 @@ function _markTreeOptional(
}
}

function _setTreeWorkspaceRef(
dependenciesMap,
depref,
pkgRefMap,
wref,
wsrcFile,
depsWorkspaceRefs,
) {
for (const dref of dependenciesMap[depref] || []) {
const addedMap = {};
const depPkg = pkgRefMap[dref];
if (!depPkg) {
continue;
}
const wsprops = depPkg.properties.filter(
(p) => p.name === "internal:workspaceRef",
);
if (wsprops.length) {
continue;
}
depPkg.properties = depPkg.properties || [];
for (const prop of depPkg.properties) {
addedMap[prop.value] = true;
}
if (!addedMap[wref]) {
depPkg.properties.push({
name: "internal:workspaceRef",
value: wref,
});
addedMap[wref] = true;
}
if (wsrcFile && !addedMap[wsrcFile]) {
depPkg.properties.push({
name: "internal:workspaceSrcFile",
value: wsrcFile,
});
addedMap[wsrcFile] = true;
}
depsWorkspaceRefs[dref] = depsWorkspaceRefs[dref] || [];
depsWorkspaceRefs[dref] = depsWorkspaceRefs[dref].concat(
dependenciesMap[depref] || [],
);
_setTreeWorkspaceRef(
dependenciesMap,
dref,
pkgRefMap,
wref,
wsrcFile,
depsWorkspaceRefs,
);
}
}

async function getVersionNumPnpm(depPkg, relativePath) {
let version = depPkg;
if (typeof version === "object" && depPkg.version) {
Expand Down Expand Up @@ -2059,6 +2112,7 @@ export async function parsePnpmLock(
const parentSubComponents = [];
const srcFilesMap = {};
const workspacePackageNames = {};
const pkgRefMap = {};
// Track references to packages that are directly installed from github.com
const gitPkgRefs = {};
// pnpm could refer to packages from git sources
Expand Down Expand Up @@ -2569,6 +2623,7 @@ export async function parsePnpmLock(
}
}
const purlNoVersion = new PackageURL("npm", group, name).toString();
let packageType = "library";
const theBomRef = decodeURIComponent(purlString);
if (
workspacePackageNames[decodeURIComponent(purlNoVersion)] ||
Expand All @@ -2578,6 +2633,7 @@ export async function parsePnpmLock(
name: "internal:is_workspace",
value: "true",
});
packageType = "application";
const wsSrcFile =
workspaceSrcFiles[decodeURIComponent(purlNoVersion)] ||
workspaceSrcFiles[theBomRef];
Expand Down Expand Up @@ -2634,6 +2690,7 @@ export async function parsePnpmLock(
version: version,
purl: purlString,
"bom-ref": theBomRef,
type: packageType,
scope,
_integrity: integrity,
properties,
Expand All @@ -2651,7 +2708,11 @@ export async function parsePnpmLock(
},
},
};
pkgList.push(thePkg);
// Don't add internal workspace packages to the components list
if (thePkg.type !== "application") {
pkgList.push(thePkg);
}
pkgRefMap[thePkg["bom-ref"]] = thePkg;
}
}
}
Expand Down Expand Up @@ -2703,35 +2764,53 @@ export async function parsePnpmLock(
if (requiredDependencies[apkg["bom-ref"]]) {
apkg.scope = undefined;
}
if (depsWorkspaceRefs[apkg["bom-ref"]]?.length) {
// There are no workspaces so exit early
if (!Object.keys(workspacePackageNames).length) {
continue;
}
const purlNoVersion = decodeURIComponent(
new PackageURL("npm", apkg.group, apkg.name).toString(),
);
const wsRefs =
depsWorkspaceRefs[apkg["bom-ref"]] || depsWorkspaceRefs[purlNoVersion];
// There is a workspace reference
if (wsRefs?.length) {
const wsprops = apkg.properties.filter(
(p) => p.name === "internal:workspaceRef",
);
if (!wsprops.length) {
for (const wref of depsWorkspaceRefs[apkg["bom-ref"]]) {
// Such a cycle should never happen, but we can't sure
if (wref === apkg["bom-ref"]) {
continue;
}
// workspace properties are already set.
if (wsprops.length) {
continue;
}
for (const wref of wsRefs) {
// Such a cycle should never happen, but we can't sure
if (wref === apkg["bom-ref"]) {
continue;
}
apkg.properties.push({
name: "internal:workspaceRef",
value: wref,
});
const purlObj = PackageURL.fromString(apkg.purl);
purlObj.version = undefined;
const wrefNoVersion = decodeURIComponent(purlObj.toString());
const wsrcFile =
workspaceSrcFiles[wref] || workspaceSrcFiles[wrefNoVersion];
if (wsrcFile) {
apkg.properties.push({
name: "internal:workspaceRef",
value: wref,
name: "internal:workspaceSrcFile",
value: wsrcFile,
});
const purlObj = PackageURL.fromString(apkg.purl);
purlObj.version = undefined;
const wrefNoVersion = decodeURIComponent(purlObj.toString());
if (workspaceSrcFiles[wref]) {
apkg.properties.push({
name: "internal:workspaceSrcFile",
value: workspaceSrcFiles[wref],
});
} else if (workspaceSrcFiles[wrefNoVersion]) {
apkg.properties.push({
name: "internal:workspaceSrcFile",
value: workspaceSrcFiles[wrefNoVersion],
});
}
}
// Repeat for the children
_setTreeWorkspaceRef(
dependenciesMap,
apkg["bom-ref"],
pkgRefMap,
wref,
wsrcFile,
depsWorkspaceRefs,
);
}
}
}
Expand Down Expand Up @@ -4915,7 +4994,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
// This would help the lookup
existingPkgMap[pkg.name.toLowerCase()] = pkg["bom-ref"];
pkgBomRefMap[pkg["bom-ref"]] = pkg;
pkgList.push(pkg);
// Do not repeat workspace components again under components
// This will reduce false positives, when a downstream tool attempts to analyze all components
if (pkg.type !== "application") {
pkgList.push(pkg);
}
if (!depsMap[pkg["bom-ref"]]) {
depsMap[pkg["bom-ref"]] = new Set();
}
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3498,6 +3498,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/@babel/[email protected]",
purl: "pkg:npm/%40babel/[email protected]",
scope: undefined,
type: "library",
version: "7.10.1",
properties: [
{
Expand Down Expand Up @@ -3530,6 +3531,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/@babel/[email protected]",
purl: "pkg:npm/%40babel/[email protected]",
scope: "optional",
type: "library",
version: "7.16.7",
properties: [
{
Expand Down Expand Up @@ -3561,6 +3563,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/[email protected]",
purl: "pkg:npm/[email protected]",
scope: undefined,
type: "library",
_integrity: "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock2.yaml" }],
evidence: {
Expand Down Expand Up @@ -3597,6 +3600,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/@nodelib/[email protected]",
purl: "pkg:npm/%40nodelib/[email protected]",
scope: undefined,
type: "library",
_integrity:
"sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock3.yaml" }],
Expand Down Expand Up @@ -3632,6 +3636,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/@babel/[email protected]",
purl: "pkg:npm/%40babel/[email protected]",
scope: "optional",
type: "library",
_integrity:
"sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" }],
Expand All @@ -3656,6 +3661,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/[email protected]",
purl: "pkg:npm/[email protected]",
scope: "optional",
type: "library",
_integrity:
"sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" }],
Expand Down Expand Up @@ -3683,6 +3689,7 @@ test("parsePnpmLock", async () => {
"bom-ref": "pkg:npm/@babel/[email protected]",
purl: "pkg:npm/%40babel/[email protected]",
scope: "optional",
type: "library",
_integrity:
"sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6a.yaml" }],
Expand Down Expand Up @@ -3764,6 +3771,7 @@ test("parsePnpmLock", async () => {
version: "2.3.0",
purl: "pkg:npm/%40ampproject/[email protected]",
"bom-ref": "pkg:npm/@ampproject/[email protected]",
type: "library",
_integrity:
"sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
properties: [{ name: "SrcFile", value: "./pnpm-lock.yaml" }],
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export function validateProps(bomJson) {
if (
isWorkspaceMode &&
!workspacePropFound &&
!srcFilePropFound &&
comp?.scope !== "optional"
) {
warningsList.push(
Expand Down
Loading

0 comments on commit 459dfe1

Please sign in to comment.