@@ -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