Skip to content

Commit 98766fe

Browse files
committed
packagedcode: add DotNetDepsJsonHandler to parse .deps.json files
Add support for parsing NuGet .deps.json lockfiles which are present alongside .dll and .pdb files in .NET projects and NuGet packages. Extracts package names, versions, types and dependencies from the libraries section of .deps.json files. Fixes #4496 Signed-off-by: kumarasantosh <santosh.pulikond02@gmail.com>
1 parent d320c97 commit 98766fe

File tree

5 files changed

+1235
-0
lines changed

5 files changed

+1235
-0
lines changed

src/packagedcode/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
nuget.NugetNupkgHandler,
157157
nuget.NugetNuspecHandler,
158158
nuget.NugetPackagesLockHandler,
159+
nuget.DotNetDepsJsonHandler,
159160

160161
opam.OpamFileHandler,
161162

src/packagedcode/nuget.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,121 @@ def parse(cls, location, package_only=False):
265265
)
266266
yield models.PackageData.from_data(package_data, package_only)
267267

268+
269+
class DotNetDepsJsonHandler(models.DatafileHandler):
270+
datasource_id = 'nuget_deps_json'
271+
path_patterns = ('*.deps.json',)
272+
default_package_type = 'nuget'
273+
description = 'NuGet .deps.json lockfile'
274+
documentation_url = 'https://github.com/dotnet/sdk/blob/main/documentation/specs/runtime-configuration-file.md'
275+
276+
@classmethod
277+
def parse(cls, location, package_only=False):
278+
with open(location) as loc:
279+
try:
280+
parsed = json.load(loc)
281+
except Exception:
282+
return
283+
284+
if not parsed or not isinstance(parsed, dict):
285+
return
286+
287+
libraries = parsed.get('libraries')
288+
if not libraries or not isinstance(libraries, dict):
289+
return
290+
291+
target_framework = None
292+
runtime_target = parsed.get('runtimeTarget')
293+
if runtime_target and isinstance(runtime_target, dict):
294+
target_framework = runtime_target.get('name')
295+
296+
# Collect target sections to look up dependencies. We prefer the runtime
297+
# target when available, but fall back to other targets to avoid missing
298+
# dependencies in valid .deps.json files without runtimeTarget.
299+
targets_dict = parsed.get('targets') or {}
300+
if not isinstance(targets_dict, dict):
301+
targets_dict = {}
302+
303+
available_targets = [
304+
(target_name, target_obj)
305+
for target_name, target_obj in targets_dict.items()
306+
if isinstance(target_obj, dict)
307+
]
308+
309+
runtime_target_matched = False
310+
if target_framework:
311+
selected_targets = [
312+
(target_name, target_obj)
313+
for target_name, target_obj in available_targets
314+
if target_name == target_framework
315+
]
316+
runtime_target_matched = bool(selected_targets)
317+
else:
318+
selected_targets = []
319+
320+
if not selected_targets:
321+
selected_targets = available_targets
322+
323+
for lib_key, lib_info in libraries.items():
324+
if not lib_key or '/' not in lib_key:
325+
continue
326+
if not isinstance(lib_info, dict):
327+
continue
328+
329+
name, version = lib_key.split('/', 1)
330+
331+
package_type = lib_info.get('type')
332+
333+
# Extract dependencies from targets
334+
dependencies = []
335+
seen_dependencies = set()
336+
for scope, target_obj in selected_targets:
337+
target_lib = target_obj.get(lib_key) or {}
338+
if not isinstance(target_lib, dict):
339+
continue
340+
341+
deps = target_lib.get('dependencies')
342+
if not deps or not isinstance(deps, dict):
343+
continue
344+
345+
for dep_name, dep_version in deps.items():
346+
if not dep_name:
347+
continue
348+
349+
dep_key = (dep_name, dep_version, scope)
350+
if dep_key in seen_dependencies:
351+
continue
352+
seen_dependencies.add(dep_key)
353+
354+
dependencies.append(
355+
models.DependentPackage(
356+
purl=str(PackageURL(type='nuget', name=dep_name, version=dep_version)),
357+
extracted_requirement=dep_version,
358+
scope=scope,
359+
is_runtime=True,
360+
is_optional=False,
361+
is_pinned=True,
362+
is_direct=True,
363+
).to_dict()
364+
)
365+
366+
extra_data = {}
367+
if runtime_target_matched:
368+
extra_data['target_framework'] = target_framework
369+
elif len(selected_targets) == 1:
370+
extra_data['target_framework'] = selected_targets[0][0]
371+
elif selected_targets:
372+
extra_data['target_frameworks'] = [scope for scope, _target_obj in selected_targets]
373+
374+
if package_type:
375+
extra_data['type'] = package_type
376+
377+
package_data = dict(
378+
datasource_id=cls.datasource_id,
379+
type=cls.default_package_type,
380+
name=name,
381+
version=version,
382+
dependencies=dependencies,
383+
extra_data=extra_data,
384+
)
385+
yield models.PackageData.from_data(package_data, package_only)

0 commit comments

Comments
 (0)