From aca22cf8bfa4a8865322969888cbb23288162e07 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Wed, 17 Jun 2026 19:50:34 +0530 Subject: [PATCH 1/5] Allow pnpm scan to complete successfully with empty BOM when packages(dependencies) are absent --- .../lockfile/process/PnpmLockYamlParserInitial.java | 13 ++++++++++++- .../pnpm/lockfile/process/PnpmYamlTransformer.java | 4 ++-- .../lockfile/process/PnpmYamlTransformerv5.java | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java index dfaa5cba88..f6d979834c 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java @@ -4,9 +4,12 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -29,6 +32,8 @@ */ public class PnpmLockYamlParserInitial { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final EnumListFilter dependencyFilter; private final List excludedDirectories; private final List includedDirectories; @@ -42,7 +47,13 @@ public PnpmLockYamlParserInitial(PnpmLockOptions pnpmLockOptions) { public List parse(File pnpmLockYamlFile, @Nullable NameVersion projectNameVersion, PnpmLinkedPackageResolver linkedPackageResolver) throws IOException, IntegrationException { PnpmLockYamlBase pnpmLockYaml = parseYamlFile(pnpmLockYamlFile); - + + if (pnpmLockYaml == null) { + logger.warn("The pnpm-lock.yaml file '{}' is empty and contains no parsable content. No dependencies will be extracted.", + pnpmLockYamlFile.getAbsolutePath()); + return Collections.emptyList(); + } + if (pnpmLockYaml instanceof PnpmLockYaml) { PnpmYamlTransformer pnpmYamlTransformer = new PnpmYamlTransformer(dependencyFilter, pnpmLockYaml.lockfileVersion); PnpmLockYamlParser pnpmYamlParser = new PnpmLockYamlParser(pnpmYamlTransformer); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java index a97993154d..7f0a2f6bf0 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java @@ -19,7 +19,6 @@ import com.blackduck.integration.bdio.model.dependency.Dependency; import com.blackduck.integration.bdio.model.externalid.ExternalId; import com.blackduck.integration.detectable.detectable.codelocation.CodeLocation; -import com.blackduck.integration.detectable.detectable.exception.DetectableException; import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectables.pnpm.lockfile.model.PnpmDependencyInfo; import com.blackduck.integration.detectable.detectables.pnpm.lockfile.model.PnpmDependencyType; @@ -80,7 +79,8 @@ private void buildGraph( @Nullable Map snapshots ) throws IntegrationException { if (packageMap == null) { - throw new DetectableException("Could not parse 'packages' section of the pnpm-lock.yaml file."); + logger.warn("The 'packages' section is absent from the pnpm-lock.yaml file. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + return; } for (Map.Entry packageEntry : packageMap.entrySet()) { String packageId = packageEntry.getKey(); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java index 488fd64e87..4f778f4613 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java @@ -18,7 +18,6 @@ import com.blackduck.integration.bdio.model.dependency.Dependency; import com.blackduck.integration.bdio.model.externalid.ExternalId; import com.blackduck.integration.detectable.detectable.codelocation.CodeLocation; -import com.blackduck.integration.detectable.detectable.exception.DetectableException; import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectables.pnpm.lockfile.model.PnpmDependencyType; import com.blackduck.integration.detectable.detectables.pnpm.lockfile.model.PnpmLockYamlv5; @@ -73,7 +72,8 @@ private void buildGraph( @Nullable String reportingProjectPackagePath ) throws IntegrationException { if (packageMap == null) { - throw new DetectableException("Could not parse 'packages' section of the pnpm-lock.yaml file."); + logger.warn("The 'packages' section is absent from the pnpm-lock.yaml file. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + return; } for (Map.Entry packageEntry : packageMap.entrySet()) { String packageId = packageEntry.getKey(); From 78f1f21b238173a2decd69d05eb18a7a8ecc77e8 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Thu, 18 Jun 2026 12:52:16 +0530 Subject: [PATCH 2/5] pnpm: Add graceful handling for empty lock files and missing lockfileVersion --- .../lockfile/process/PnpmLockYamlParser.java | 16 +++++++++++++++- .../process/PnpmLockYamlParserInitial.java | 5 +++++ .../lockfile/process/PnpmLockYamlParserv5.java | 10 ++++++++++ .../lockfile/process/PnpmYamlTransformer.java | 3 ++- .../lockfile/process/PnpmYamlTransformerv5.java | 3 ++- 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParser.java index 458f7c676a..d1f1982aab 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParser.java @@ -10,6 +10,8 @@ import org.apache.commons.collections4.Predicate; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.blackduck.integration.detectable.detectable.codelocation.CodeLocation; import com.blackduck.integration.detectable.detectables.pnpm.lockfile.model.PnpmLockYaml; @@ -19,6 +21,8 @@ import com.blackduck.integration.util.NameVersion; public class PnpmLockYamlParser { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private static final Predicate isNodeRoot = "."::equals; private PnpmYamlTransformer pnpmTransformer; @@ -41,6 +45,9 @@ public List parse(File parentFile, PnpmLockYaml pnpmLockYaml, private List createCodeLocationsFromRoot(File sourcePath, PnpmLockYaml pnpmLockYaml, @Nullable NameVersion projectNameVersion, PnpmLinkedPackageResolver linkedPackageResolver) throws IntegrationException { + if (pnpmLockYaml.packages == null) { + logger.warn("The pnpm-lock.yaml file has no 'packages' section. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + } CodeLocation codeLocation = pnpmTransformer.generateCodeLocation(sourcePath, pnpmLockYaml, projectNameVersion, linkedPackageResolver); return Collections.singletonList(codeLocation); @@ -52,7 +59,14 @@ private List createCodeLocationsFromImports(File sourcePath, PnpmL if (MapUtils.isEmpty(pnpmLockYaml.importers)) { return Collections.emptyList(); } - + + if (pnpmLockYaml.packages == null) { + logger.warn("The pnpm-lock.yaml file contains {} importer(s) {} but has no 'packages' section. " + + "No resolved dependencies are available. All workspaces will have empty dependency graphs.", + pnpmLockYaml.importers.size(), + pnpmLockYaml.importers.keySet()); + } + ExcludedIncludedWildcardFilter workspacesFilter; if (excludedDirectories.isEmpty() && includedDirectories.isEmpty()) { workspacesFilter = null; // Include all diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java index f6d979834c..deae563728 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java @@ -55,6 +55,11 @@ public List parse(File pnpmLockYamlFile, @Nullable NameVersion pro } if (pnpmLockYaml instanceof PnpmLockYaml) { + if (pnpmLockYaml.lockfileVersion == null) { + throw new IntegrationException( + "The pnpm-lock.yaml file does not contain a 'lockfileVersion' field. " + + "This is required for parsing. Please regenerate the lock file by running 'pnpm install'."); + } PnpmYamlTransformer pnpmYamlTransformer = new PnpmYamlTransformer(dependencyFilter, pnpmLockYaml.lockfileVersion); PnpmLockYamlParser pnpmYamlParser = new PnpmLockYamlParser(pnpmYamlTransformer); return pnpmYamlParser.parse(pnpmLockYamlFile.getParentFile(), (PnpmLockYaml) pnpmLockYaml, linkedPackageResolver, projectNameVersion, excludedDirectories, includedDirectories); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserv5.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserv5.java index 265c14439e..c364c6c07c 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserv5.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserv5.java @@ -47,6 +47,9 @@ private List createCodeLocationsFromRoot( @Nullable NameVersion projectNameVersion, PnpmLinkedPackageResolver linkedPackageResolver ) throws IntegrationException { + if (pnpmLockYaml.packages == null) { + logger.warn("The pnpm-lock.yaml file has no 'packages' section. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + } CodeLocation codeLocation = pnpmTransformer.generateCodeLocation(sourcePath, pnpmLockYaml, projectNameVersion, linkedPackageResolver); return Collections.singletonList(codeLocation); } @@ -61,6 +64,13 @@ private List createCodeLocationsFromImports( return Collections.emptyList(); } + if (pnpmLockYaml.packages == null) { + logger.warn("The pnpm-lock.yaml file contains {} importer(s) {} but has no 'packages' section. " + + "No resolved dependencies are available. All workspaces will have empty dependency graphs.", + pnpmLockYaml.importers.size(), + pnpmLockYaml.importers.keySet()); + } + List codeLocations = new LinkedList<>(); for (Map.Entry projectPackageInfo : pnpmLockYaml.importers.entrySet()) { String projectKey = projectPackageInfo.getKey(); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java index 7f0a2f6bf0..efeccc0f1b 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformer.java @@ -79,7 +79,8 @@ private void buildGraph( @Nullable Map snapshots ) throws IntegrationException { if (packageMap == null) { - logger.warn("The 'packages' section is absent from the pnpm-lock.yaml file. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + logger.debug("No packages available to build dependency graph for workspace '{}'. Skipping graph construction.", + reportingProjectPackagePath != null ? reportingProjectPackagePath : "root"); return; } for (Map.Entry packageEntry : packageMap.entrySet()) { diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java index 4f778f4613..c8db5e64fd 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmYamlTransformerv5.java @@ -72,7 +72,8 @@ private void buildGraph( @Nullable String reportingProjectPackagePath ) throws IntegrationException { if (packageMap == null) { - logger.warn("The 'packages' section is absent from the pnpm-lock.yaml file. No resolved dependencies are present. The scan will continue with an empty dependency graph."); + logger.debug("No packages available to build dependency graph for workspace '{}'. Skipping graph construction.", + reportingProjectPackagePath != null ? reportingProjectPackagePath : "root"); return; } for (Map.Entry packageEntry : packageMap.entrySet()) { From 9df98ec5b4c48ff4de640ea24bc4591a5c7c5896 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Fri, 19 Jun 2026 12:09:28 +0530 Subject: [PATCH 3/5] Replace exception-based v5 fallback with result validation --- .../process/PnpmLockYamlParserInitial.java | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java index deae563728..0d1b8aa596 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java @@ -45,7 +45,7 @@ public PnpmLockYamlParserInitial(PnpmLockOptions pnpmLockOptions) { } public List parse(File pnpmLockYamlFile, @Nullable NameVersion projectNameVersion, PnpmLinkedPackageResolver linkedPackageResolver) - throws IOException, IntegrationException { + throws IOException, IntegrationException, ConstructorException { PnpmLockYamlBase pnpmLockYaml = parseYamlFile(pnpmLockYamlFile); if (pnpmLockYaml == null) { @@ -86,15 +86,36 @@ private PnpmLockYamlBase parseYamlFile(File pnpmLockYamlFile) throws FileNotFoun representer.getPropertyUtils().setSkipMissingProperties(true); LoaderOptions loaderOptions = new LoaderOptions(); + try { - // Try to read the lockfile into the current Yaml classes. It's more common and - // should hopefully work more of the time. + // Try to read the lockfile into the v6/v9 Yaml classes first (more common). + logger.debug("Parsing through v6/v9 format"); Yaml yaml = new Yaml(new Constructor(PnpmLockYaml.class, loaderOptions), representer); - return yaml.load(new FileReader(pnpmLockYamlFile)); + PnpmLockYamlBase result = yaml.load(new FileReader(pnpmLockYamlFile)); + + // If we got a valid result with a v6+ lockfileVersion, use it. + if (result != null && isV6OrNewer(result.lockfileVersion)) { + return result; + } } catch (ConstructorException e) { - // If the reading fails try to read a v5 Yaml. - Yaml yaml = new Yaml(new Constructor(PnpmLockYamlv5.class, loaderOptions), representer); - return yaml.load(new FileReader(pnpmLockYamlFile)); + // Fall through to try v5 parsing + } + + // Either: lockfileVersion was null, indicated v5, or a ConstructorException was thrown. + // Re-parse as v5. + logger.debug("Parsing through v5 format"); + Yaml yaml = new Yaml(new Constructor(PnpmLockYamlv5.class, loaderOptions), representer); + return yaml.load(new FileReader(pnpmLockYamlFile)); + } + + /** + * Returns true if the lockfileVersion string represents a v6 or newer pnpm lockfile. + * Returns false if the version is v5 or older, or null. + */ + private boolean isV6OrNewer(@Nullable String lockfileVersion) { + if (lockfileVersion == null) { + return false; } + return Double.parseDouble(lockfileVersion) >= 6.0; } } From 491d7418b36ee36d2053668583cf5ba10431d59a Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Tue, 23 Jun 2026 11:48:40 +0530 Subject: [PATCH 4/5] Gracefully handling for non-numeric lockfileversion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../pnpm/lockfile/process/PnpmLockYamlParserInitial.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java index 0d1b8aa596..aee4c41493 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java @@ -116,6 +116,11 @@ private boolean isV6OrNewer(@Nullable String lockfileVersion) { if (lockfileVersion == null) { return false; } - return Double.parseDouble(lockfileVersion) >= 6.0; + try { + return Double.parseDouble(lockfileVersion) >= 6.0; + } catch (NumberFormatException e) { + logger.debug("Unable to parse lockfileVersion '{}'; treating as v5/unknown.", lockfileVersion); + return false; + } } } From ebce8dbd53c275b9f674959b91d00dc12986cfc3 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Tue, 23 Jun 2026 12:24:04 +0530 Subject: [PATCH 5/5] Remove ConstructorException from parse() method --- .../pnpm/lockfile/process/PnpmLockYamlParserInitial.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java index aee4c41493..7f534ad7e6 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/pnpm/lockfile/process/PnpmLockYamlParserInitial.java @@ -45,7 +45,7 @@ public PnpmLockYamlParserInitial(PnpmLockOptions pnpmLockOptions) { } public List parse(File pnpmLockYamlFile, @Nullable NameVersion projectNameVersion, PnpmLinkedPackageResolver linkedPackageResolver) - throws IOException, IntegrationException, ConstructorException { + throws IOException, IntegrationException { PnpmLockYamlBase pnpmLockYaml = parseYamlFile(pnpmLockYamlFile); if (pnpmLockYaml == null) {