99
1010import logging
1111import sys
12+ import warnings
1213
1314import attr
1415import saneyaml
@@ -58,8 +59,8 @@ def logger_debug(*args):
5859
5960@attr .s ()
6061class PubspecPackage (models .Package ):
61- metafiles = ('pubspec.yaml' ,)
62- extensions = ('.yaml' ,)
62+ metafiles = ('pubspec.yaml' , 'pubspec.lock' , )
63+ extensions = ('.yaml' , '.lock' , )
6364 default_type = 'pubspec'
6465 default_primary_language = 'dart'
6566 default_web_baseurl = 'https://pub.dev/packages'
@@ -68,7 +69,10 @@ class PubspecPackage(models.Package):
6869
6970 @classmethod
7071 def recognize (cls , location ):
71- yield parse_pub (location )
72+ if is_pubspec_yaml (location ):
73+ yield parse_pub (location )
74+ elif is_pubspec_lock (location ):
75+ yield parse_lock (location )
7276
7377 def repository_homepage_url (self , baseurl = default_web_baseurl ):
7478 return f'{ baseurl } /{ self .name } /versions/{ self .version } '
@@ -116,7 +120,7 @@ def parse_pub(location, compute_normalized_license=False):
116120 Return a PubspecPackage constructed from the pubspec.yaml file at ``location``
117121 or None.
118122 """
119- if not is_pubspec (location ):
123+ if not is_pubspec_yaml (location ):
120124 return
121125 with open (location ) as inp :
122126 package_data = saneyaml .load (inp .read ())
@@ -127,44 +131,172 @@ def parse_pub(location, compute_normalized_license=False):
127131 return package
128132
129133
130- def is_pubspec (location ):
134+ def file_endswith (location , endswith ):
131135 """
132- Check if the file is a yaml file or not
136+ Check if the file at ``location`` ends with ``endswith`` string or tuple.
133137 """
134- return filetype .is_file (location ) and location .endswith ('pubspec.yaml' )
138+ return filetype .is_file (location ) and location .endswith (endswith )
135139
136140
137- def collect_deps (data , dependency_field_name , is_runtime = True , is_optional = False ):
141+ def is_pubspec_yaml (location ):
142+ return file_endswith (location , 'pubspec.yaml' )
143+
144+
145+ def is_pubspec_lock (location ):
146+ return file_endswith (location , 'pubspec.lock' )
147+
148+
149+ def parse_lock (location ):
138150 """
139- Yield DependentPackage found in the ``dependency_field_name`` of ``data``
151+ Yield PubspecPackages dependencies constructed from the pubspec.lock file at
152+ ``location``.
153+ """
154+ if not is_pubspec_lock (location ):
155+ return
156+
157+ with open (location ) as inp :
158+ locks_data = saneyaml .load (inp .read ())
159+
160+ return PubspecPackage (dependencies = list (collect_locks (locks_data )))
161+
162+
163+ def collect_locks (locks_data ):
164+ """
165+ Yield DependentPackage from locks data
166+
167+ The general form is
168+ packages:
169+ _fe_analyzer_shared:
170+ dependency: transitive
171+ description:
172+ name: _fe_analyzer_shared
173+ url: "https://pub.dartlang.org"
174+ source: hosted
175+ version: "22.0.0"
176+ sdks:
177+ dart: ">=2.12.0 <3.0.0"
178+ """
179+ # FIXME: we treat all as nno optioanl for now
180+ sdks = locks_data .get ('sdks' ) or {}
181+ for name , version in sdks .items ():
182+ dep = build_dep (
183+ name ,
184+ version ,
185+ scope = 'sdk' ,
186+ is_runtime = True ,
187+ is_optional = False ,
188+ )
189+ yield dep
190+
191+ packages = locks_data .get ('packages' ) or {}
192+ for name , details in packages .items ():
193+ version = details .get ('version' )
194+
195+ # FIXME: see https://github.com/dart-lang/pub/blob/2a08832e0b997ff92de65571b6d79a9b9099faa0/lib/src/lock_file.dart#L344
196+ # transitive, direct main, direct dev, direct overridden.
197+ # they do not map exactly to the pubspec scopes since transitive can be
198+ # either main or dev
199+ scope = details .get ('dependency' )
200+ if scope == 'direct dev' :
201+ is_runtime = False
202+ else :
203+ is_runtime = True
204+
205+ desc = details .get ('description' ) or {}
206+ known_desc = isinstance (desc , dict )
207+
208+ # issue a warning for unknown data structure
209+ warn = False
210+ if not known_desc :
211+ if not (isinstance (desc , str ) and desc == 'flutter' ):
212+ warn = True
213+ else :
214+ dname = desc .get ('name' )
215+ durl = desc .get ('url' )
216+ dsource = details .get ('source' )
217+
218+ if (
219+ (dname and dname != name )
220+ or (durl and durl != 'https://pub.dartlang.org' )
221+ or (dsource and dsource not in ['hosted' , 'sdk' , ])
222+ ):
223+ warn = True
224+
225+ if warn :
226+ warnings .warn (
227+ f'Dart pubspec.locks with unsupported external repo '
228+ f'description or source: { details } ' ,
229+ stacklevel = 1 ,
230+ )
231+
232+ dep = build_dep (
233+ name ,
234+ version ,
235+ scope = scope ,
236+ is_runtime = is_runtime ,
237+ is_optional = False ,
238+ )
239+ yield dep
240+
241+
242+ def collect_deps (data , dependency_field_name , is_runtime = True , is_optional = False ):
140243 """
244+ Yield DependentPackage found in the ``dependency_field_name`` of ``data``.
245+ Use is_runtime and is_optional in created DependentPackage.
141246
247+ The shape of the data is:
248+ dependencies:
249+ path: 1.7.0
250+ meta: ^1.2.4
251+ yaml: ^3.1.0
252+
253+ environment:
254+ sdk: '>=2.12.0 <3.0.0'
255+ """
142256 # TODO: these can be more complex for SDKs
143257 # https://dart.dev/tools/pub/dependencies#dependency-sources
144-
145258 dependencies = data .get (dependency_field_name ) or {}
146259 for name , version in dependencies .items ():
147- if isinstance (version , dict ) and 'sdk' in version :
148- # {'sdk': 'flutter'} type of deps....
149- # which is a wart that we keep as a requiremnet
150- version = ', ' .join (': ' .join ([k , str (v )]) for k , v in version .items ())
151-
152- if version .replace ('.' , '' ).isdigit ():
153- # version is pinned exactly if it is only made of dots and digits
154- purl = PackageURL (type = 'pubspec' , name = name , version = version )
155- is_resolved = True
156- else :
157- purl = PackageURL (type = 'pubspec' , name = name )
158- is_resolved = False
159-
160- yield models .DependentPackage (
161- purl = purl .to_string (),
162- requirement = version ,
260+ dep = build_dep (
261+ name ,
262+ version ,
163263 scope = dependency_field_name ,
164264 is_runtime = is_runtime ,
165265 is_optional = is_optional ,
166- is_resolved = is_resolved ,
167266 )
267+ yield dep
268+
269+
270+ def build_dep (name , version , scope , is_runtime = True , is_optional = False ):
271+ """
272+ Return DependentPackage from the provided data.
273+ """
274+
275+ # TODO: these can be more complex for SDKs
276+ # https://dart.dev/tools/pub/dependencies#dependency-sources
277+
278+ if isinstance (version , dict ) and 'sdk' in version :
279+ # {'sdk': 'flutter'} type of deps....
280+ # which is a wart that we keep as a requiremnet
281+ version = ', ' .join (': ' .join ([k , str (v )]) for k , v in version .items ())
282+
283+ if version .replace ('.' , '' ).isdigit ():
284+ # version is pinned exactly if it is only made of dots and digits
285+ purl = PackageURL (type = 'pubspec' , name = name , version = version )
286+ is_resolved = True
287+ else :
288+ purl = PackageURL (type = 'pubspec' , name = name )
289+ is_resolved = False
290+
291+ dep = models .DependentPackage (
292+ purl = purl .to_string (),
293+ requirement = version ,
294+ scope = scope ,
295+ is_runtime = is_runtime ,
296+ is_optional = is_optional ,
297+ is_resolved = is_resolved ,
298+ )
299+ return dep
168300
169301
170302def build_package (pubspec_data ):
0 commit comments