@@ -30,48 +30,95 @@ def parse_package_json(text: str) -> List[Dict[str, Any]]:
3030 deps .append ({"name" : name , "version" : version })
3131 return deps
3232
33+ def parse_csproj_file (content : str ) -> List [Dict [str , Any ]]:
34+ """Parse a .csproj file content and extract PackageReference entries.
3335
34- def parse_requirements (text : str ) -> List [Dict [str , Any ]]:
36+ Returns a list of dicts with keys: `name` and `version` (version may be None).
37+ Handles both attribute-style (`<PackageReference Include="Foo" Version="1.2.3" />`)
38+ and nested `<Version>` child elements. Works with XML namespaces.
39+ """
40+ try :
41+ root = ET .fromstring (content )
42+ except ET .ParseError :
43+ return []
44+
45+ deps : List [Dict [str , Any ]] = []
46+
47+ def local_name (tag : str ) -> str :
48+ return tag .split ('}' )[- 1 ] if '}' in tag else tag
49+
50+ # Iterate through all elements and find PackageReference nodes
51+ for elem in root .iter ():
52+ if local_name (elem .tag ) != 'PackageReference' :
53+ continue
54+
55+ # Name can be in Include or Update attribute
56+ name = elem .get ('Include' ) or elem .get ('Update' )
57+
58+ # Version can be an attribute or a child <Version> element
59+ version = elem .get ('Version' )
60+ if version is None :
61+ for child in list (elem ):
62+ if local_name (child .tag ) == 'Version' and (child .text or '' ).strip ():
63+ version = (child .text or '' ).strip ()
64+ break
65+
66+ if name :
67+ deps .append ({"name" : name , "version" : version })
68+
69+ return deps
70+
71+ def parse_requirements (text : str , filename : str = "" ) -> List [Dict [str , Any ]]:
3572 # Support two common formats:
3673 # 1) pip-style requirements (lines with optional ==version)
3774 # 2) NuGet packages.config XML
3875 text_stripped = text .strip ()
39- # Heuristic: if it looks like XML and contains <package ... />, parse as packages.config
40- if text_stripped .startswith ("<?xml" ) or "<packages" in text_stripped or "<package" in text_stripped :
41- deps : List [Dict [str , Any ]] = []
42- try :
43- root = ET .fromstring (text )
44- # Handle <packages><package id="..." version="..." /></packages>
45- for pkg in root .findall ('.//package' ):
46- name = pkg .get ('id' ) or pkg .get ('Id' ) or pkg .get ('name' )
47- version = pkg .get ('version' ) or pkg .get ('Version' )
48- if name :
49- deps .append ({"name" : name , "version" : version })
50- except ET .ParseError :
51- # Fall back to pip-style parsing if XML parsing fails
52- deps = []
53- for raw in text .splitlines ():
54- line = raw .strip ()
55- if not line or line .startswith ("#" ):
56- continue
57- if "==" in line :
58- name , ver = line .split ("==" , 1 )
59- deps .append ({"name" : name .strip (), "version" : ver .strip ()})
60- else :
61- deps .append ({"name" : line , "version" : None })
62- return deps
63-
64- # Default: pip-style requirements
65- deps = []
66- for raw in text .splitlines ():
67- line = raw .strip ()
68- if not line or line .startswith ("#" ):
69- continue
70- if "==" in line :
71- name , ver = line .split ("==" , 1 )
72- deps .append ({"name" : name .strip (), "version" : ver .strip ()})
73- else :
74- deps .append ({"name" : line , "version" : None })
76+ deps : List [Dict [str , Any ]] = []
77+
78+ if filename and filename .lower ().endswith (".csproj" ):
79+ deps = parse_csproj_file (content = text_stripped )
80+ elif (filename and filename .lower ().find ("packages.config" ) != - 1 ):
81+ return parse_packages_config (text )
82+ else :
83+ # Default: pip-style requirements
84+ deps = []
85+ for raw in text .splitlines ():
86+ line = raw .strip ()
87+ if not line or line .startswith ("#" ):
88+ continue
89+ if "==" in line :
90+ name , ver = line .split ("==" , 1 )
91+ deps .append ({"name" : name .strip (), "version" : ver .strip ()})
92+ else :
93+ deps .append ({"name" : line , "version" : None })
94+ return deps
95+
96+
97+ def parse_packages_config (text : str ) -> List [Dict [str , Any ]]:
98+ """Parse a NuGet `packages.config` XML or fall back to pip-style lines.
99+
100+ Returns list of {name, version}.
101+ """
102+ deps : List [Dict [str , Any ]] = []
103+ try :
104+ root = ET .fromstring (text )
105+ # Handle <packages><package id="..." version="..." /></packages>
106+ for pkg in root .findall ('.//package' ):
107+ name = pkg .get ('id' ) or pkg .get ('Id' ) or pkg .get ('name' )
108+ version = pkg .get ('version' ) or pkg .get ('Version' )
109+ if name :
110+ deps .append ({"name" : name , "version" : version })
111+ except ET .ParseError :
112+ # Fall back to pip-style parsing if XML parsing fails
113+ for raw in text .splitlines ():
114+ line = raw .strip ()
115+ if not line or line .startswith ("#" ):
116+ continue
117+ if "==" in line :
118+ name , ver = line .split ("==" , 1 )
119+ deps .append ({"name" : name .strip (), "version" : ver .strip ()})
120+ else :
121+ deps .append ({"name" : line , "version" : None })
75122 return deps
76123
77124
@@ -88,7 +135,7 @@ def analyze_file(filename: str, content: str) -> Dict[str, Any]:
88135 elif manager == "maven" :
89136 result ["ecosystem" ] = "maven"
90137 elif manager == "nuget" :
91- result ["dependencies" ] = parse_requirements (content )
138+ result ["dependencies" ] = parse_requirements (content , filename = filename )
92139 result ["ecosystem" ] = "nuget"
93140 else :
94141 result ["ecosystem" ] = "unknown"
0 commit comments