From 4155b4d07b648ff610e3fcd2fef2f0e53733cb25 Mon Sep 17 00:00:00 2001 From: Marco Berger Date: Wed, 3 Jun 2026 21:38:08 +0200 Subject: [PATCH] Handle NuGet lock dependency types Support NuGet packages.lock.json files with Project and CentralTransitive dependency entries. * Skip Project entries because they are project references, not NuGet package dependencies. * Treat CentralTransitive entries as transitive package dependencies. This prevents parsing from aborting for lockfiles generated by projects using project references and Central Package Management. Signed-off-by: Marco Berger --- AUTHORS.rst | 1 + CHANGELOG.rst | 7 +++ src/packagedcode/nuget.py | 56 +++++++++++-------- ...h-project-and-central-transitive.lock.json | 33 +++++++++++ tests/packagedcode/test_nuget.py | 36 +++++++++++- 5 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 tests/packagedcode/data/nuget/packages-with-project-and-central-transitive.lock.json diff --git a/AUTHORS.rst b/AUTHORS.rst index 06f151d5db5..c05ca488659 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -49,6 +49,7 @@ The following organizations or individuals have contributed to ScanCode: - Li Ha @linexb - Mankaran Singh @MankaranSingh - Marc-Etienne Vargenau @vargenau +- Marco Berger @marcoberger - Martin Petkov @MartinPetkov - Maximilian Huber @maxhbr - Michael Herzog @mjherzog diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9a5a6b4026..0e0829e9ba7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ with the licensedcode-data and licensedcode-index being published in two seperate wheels. Also adds linux/macos ARM support in release archives and pypi wheels. +- Fix NuGet ``packages.lock.json`` parsing to support ``Project`` and + ``CentralTransitive`` dependency types. ``Project`` entries are skipped + because they are project references, while ``CentralTransitive`` entries + are treated as transitive package dependencies. This prevents parsing from + aborting for lockfiles generated by projects using project references and + Central Package Management. + - Remove the licensedcode data and built license indexes from the main scancode-toolkit built wheel, and release them as seperate wheels which scancode-toolkit depends on. diff --git a/src/packagedcode/nuget.py b/src/packagedcode/nuget.py index d0d7e110f2f..e1cca0a782b 100644 --- a/src/packagedcode/nuget.py +++ b/src/packagedcode/nuget.py @@ -218,28 +218,36 @@ def parse(cls, location, package_only=False): extra_data = dict( target_framework=target_framework, ) + for package_name, package_info in packages.items(): - dependencies = cls.get_dependencies(package_info=package_info, scope=target_framework) - resolved_package_mapping = dict( - datasource_id=cls.datasource_id, - type=cls.default_package_type, - primary_language=cls.default_primary_language, - name=package_name, - dependencies=[ - dep.to_dict() for dep in dependencies - ], - is_virtual=True, - version=package_info.get('resolved'), - ) - resolved_package = models.PackageData.from_data(resolved_package_mapping) package_type = package_info.get('type') + + if package_type == "Project": + continue + if package_type == "Direct": is_direct = True - elif package_type == "Transitive": + elif package_type in {"Transitive", "CentralTransitive"}: is_direct = False else: - raise Exception(f"Unknown package type: {package_type} for package {package_name} in {location}") - + raise Exception( + f"Unknown package type: {package_type} " + f"for package {package_name} in {location}" + ) + + dependencies = cls.get_dependencies(package_info=package_info, scope=target_framework) + resolved_package_mapping = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language=cls.default_primary_language, + name=package_name, + dependencies=[ + dep.to_dict() for dep in dependencies + ], + is_virtual=True, + version=package_info.get('resolved'), + ) + resolved_package = models.PackageData.from_data(resolved_package_mapping) version = package_info.get('resolved') requested = package_info.get('requested') @@ -256,12 +264,12 @@ def parse(cls, location, package_only=False): is_direct=is_direct, ) top_dependencies.append(dependency.to_dict()) - package_data = dict( - datasource_id=cls.datasource_id, - type=cls.default_package_type, - primary_language=cls.default_primary_language, - extra_data=extra_data, - dependencies=top_dependencies, - ) - yield models.PackageData.from_data(package_data, package_only) + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language=cls.default_primary_language, + extra_data=extra_data, + dependencies=top_dependencies, + ) + yield models.PackageData.from_data(package_data, package_only) diff --git a/tests/packagedcode/data/nuget/packages-with-project-and-central-transitive.lock.json b/tests/packagedcode/data/nuget/packages-with-project-and-central-transitive.lock.json new file mode 100644 index 00000000000..bd763914682 --- /dev/null +++ b/tests/packagedcode/data/nuget/packages-with-project-and-central-transitive.lock.json @@ -0,0 +1,33 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "Direct.Package": { + "type": "Direct", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "direct-package-content-hash", + "dependencies": { + "Transitive.Package": "2.0.0" + } + }, + "Transitive.Package": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "transitive-package-content-hash" + }, + "CentralTransitive.Package": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "central-transitive-package-content-hash" + }, + "Local.Project": { + "type": "Project", + "dependencies": { + "CentralTransitive.Package": "[3.0.0, )" + } + } + } + } +} diff --git a/tests/packagedcode/test_nuget.py b/tests/packagedcode/test_nuget.py index d3af13ab51b..8708e0350f5 100644 --- a/tests/packagedcode/test_nuget.py +++ b/tests/packagedcode/test_nuget.py @@ -62,13 +62,45 @@ def test_parse_as_package_only(self): package = nuget.NugetNuspecHandler.parse(location=test_file, package_only=True) expected_loc = self.get_test_loc('nuget/Castle.Core.nuspec-package-only.json.expected') self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES, package_only=True) - + def test_parse_nuget_package_lock_json(self): test_file = self.get_test_loc('nuget/packages.lock.json') package = nuget.NugetPackagesLockHandler.parse(location=test_file) expected_loc = self.get_test_loc('nuget/packages.lock.json.expected') self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES, package_only=True) - + + def test_parse_nuget_package_lock_json_with_project_and_central_transitive_types(self): + test_file = self.get_test_loc( + 'nuget/packages-with-project-and-central-transitive.lock.json' + ) + + packages = list( + nuget.NugetPackagesLockHandler.parse( + location=test_file, + package_only=True, + ) + ) + + assert len(packages) == 1 + + package = packages[0].to_dict() + dependencies = package['dependencies'] + dependencies_by_purl = { + dependency['purl']: dependency + for dependency in dependencies + } + + assert 'pkg:nuget/Local.Project@1.0.0' not in dependencies_by_purl + + assert dependencies_by_purl['pkg:nuget/Direct.Package@1.0.0']['is_direct'] is True + assert dependencies_by_purl['pkg:nuget/Direct.Package@1.0.0']['extracted_requirement'] == '[1.0.0, )' + + assert dependencies_by_purl['pkg:nuget/Transitive.Package@2.0.0']['is_direct'] is False + assert dependencies_by_purl['pkg:nuget/Transitive.Package@2.0.0']['extracted_requirement'] == '2.0.0' + + assert dependencies_by_purl['pkg:nuget/CentralTransitive.Package@3.0.0']['is_direct'] is False + assert dependencies_by_purl['pkg:nuget/CentralTransitive.Package@3.0.0']['extracted_requirement'] == '[3.0.0, )' + def test_package_lock_json_is_package_data_file(self): test_file = self.get_test_loc('nuget/packages.lock.json') assert nuget.NugetPackagesLockHandler.is_datafile(test_file)