11#
2- # Copyright (c) 2015 nexB Inc. and others. All rights reserved.
2+ # Copyright (c) 2017 nexB Inc. and others. All rights reserved.
33# http://nexb.com and https://github.com/nexB/scancode-toolkit/
44# The ScanCode software is licensed under the Apache License version 2.0.
55# Data generated with ScanCode require an acknowledgment.
5050
5151
5252class PHPComposerPackage (models .Package ):
53- metafiles = ('composer.json' )
53+ metafiles = ('composer.json' , )
5454 filetypes = ('.json' ,)
55- mimetypes = ('application/json' )
55+ mimetypes = ('application/json' , )
5656 repo_types = (models .repo_phpcomposer ,)
5757
5858 type = models .StringType (default = 'phpcomposer' )
@@ -74,7 +74,24 @@ def parse(location):
7474 """
7575 if not is_phpcomposer_json (location ):
7676 return
77- # mapping of top level composer.json items to the Package object field name
77+
78+ with codecs .open (location , encoding = 'utf-8' ) as loc :
79+ package_data = json .load (loc , object_pairs_hook = OrderedDict )
80+
81+ base_dir = fileutils .parent_directory (location )
82+ metafile_name = fileutils .file_name (location )
83+
84+ return build_package (package_data , base_dir , metafile_name )
85+
86+
87+ def build_package (package_data , base_dir = None , metafile_name = 'composer.json' ):
88+ """
89+ Return a composer Package object from a package data mapping or
90+ None.
91+ """
92+
93+ # mapping of top level composer.json items to the Package object
94+ # field name
7895 plain_fields = OrderedDict ([
7996 ('name' , 'name' ),
8097 ('description' , 'summary' ),
@@ -83,8 +100,9 @@ def parse(location):
83100 ('homepage' , 'homepage_url' ),
84101 ])
85102
86- # mapping of top level composer.json items to a function accepting as arguments
87- # the composer.json element value and returning an iterable of key, values Package Object to update
103+ # mapping of top level composer.json items to a function accepting
104+ # as arguments the composer.json element value and returning an
105+ # iterable of key, values Package Object to update
88106 field_mappers = OrderedDict ([
89107 ('authors' , author_mapper ),
90108 ('license' , licensing_mapper ),
@@ -94,22 +112,20 @@ def parse(location):
94112 ('support' , support_mapper ),
95113 ])
96114
97- with codecs .open (location , encoding = 'utf-8' ) as loc :
98- data = json .load (loc , object_pairs_hook = OrderedDict )
99115
100- if not data .get ('name' ) or not data .get ('description' ):
101- # a composer.json without name and description is not a usable PHP composer package
102- # name and description fields are required: https://getcomposer.org/doc/04-schema.md#name
103- return
116+ # A composer.json without name and description is not a usable PHP
117+ # composer package. Name and description fields are required but
118+ # only for published packages:
119+ # https://getcomposer.org/doc/04-schema.md#name
120+ # We want to catch both published and non-published packages here.
104121
105122 package = PHPComposerPackage ()
106123 # a composer.json is at the root of a PHP composer package
107- base_dir = fileutils .parent_directory (location )
108124 package .location = base_dir
109- package .metafile_locations = [location ]
125+ package .metafile_locations = [metafile_name ]
110126
111127 for source , target in plain_fields .items ():
112- value = data .get (source )
128+ value = package_data .get (source )
113129 if value :
114130 if isinstance (value , basestring ):
115131 value = value .strip ()
@@ -118,13 +134,14 @@ def parse(location):
118134
119135 for source , func in field_mappers .items ():
120136 logger .debug ('parse: %(source)r, %(func)r' % locals ())
121- value = data .get (source )
137+ value = package_data .get (source )
122138 if value :
123139 if isinstance (value , basestring ):
124140 value = value .strip ()
125141 if value :
126142 func (value , package )
127- vendor_mapper (package ) # Parse vendor from name value
143+ # Parse vendor from name value
144+ vendor_mapper (package )
128145 return package
129146
130147
@@ -145,27 +162,25 @@ def licensing_mapper(licenses, package):
145162 if not licenses :
146163 return package
147164
148- if isinstance (licenses , basestring ):
149- package . asserted_licenses . append ( models . AssertedLicense ( license = licenses ))
150- elif isinstance ( licenses , list ):
165+ if isinstance (licenses , list ):
166+ # For a package, when there is a choice between licenses
167+ # ("disjunctive license"), multiple can be specified as array.
151168 """
152169 "license": [
153170 "LGPL-2.1",
154171 "GPL-3.0+"
155172 ]
156173 """
157- for lic in licenses :
158- if isinstance (lic , basestring ):
159- package .asserted_licenses .append (models .AssertedLicense (license = lic ))
160- else :
161- # use the bare repr
162- if lic :
163- package .asserted_licenses .append (models .AssertedLicense (license = repr (lic )))
174+ # build a proper license expression
175+ lics = [l .strip () for l in licenses if l and l .strip ()]
176+ lics = ' OR ' .join (lics )
164177
178+ elif not isinstance (licenses , basestring ):
179+ lics = repr (licenses )
165180 else :
166- # use the bare repr
167- package .asserted_licenses .append (models .AssertedLicense (license = repr (licenses )))
181+ lics = licenses
168182
183+ package .asserted_licenses .append (models .AssertedLicense (license = lics ))
169184 return package
170185
171186
@@ -186,22 +201,24 @@ def support_mapper(support, package):
186201 Update support and bug tracking url.
187202 https://getcomposer.org/doc/04-schema.md#support
188203 """
189- package .support_contacts = [support .get ('email' )]
190- package .bug_tracking_url = support .get ('issues' )
191- package .code_view_url = support .get ('source' )
204+ email = support .get ('email' )
205+ if email and email .strip ():
206+ package .support_contacts = [email ]
207+ package .bug_tracking_url = support .get ('issues' ) or None
208+ package .code_view_url = support .get ('source' ) or None
192209 return package
193210
194211
195212def vendor_mapper (package ):
196213 """
197- Vender is part of name element.
214+ Vendor is the first part of the name element.
198215 https://getcomposer.org/doc/04-schema.md#name
199216 """
200217 name = package .name
201218 if name and '/' in name :
202- vendors = name .split ('/' )
203- if vendors [ 0 ] :
204- package .vendors = [models .Party (name = vendors [ 0 ] )]
219+ vendor , _base_name = name .split ('/' , 1 )
220+ if vendor :
221+ package .vendors = [models .Party (name = vendor )]
205222 return package
206223
207224
@@ -326,6 +343,7 @@ def parse_person(persons):
326343 name = person .get ('name' )
327344 email = person .get ('email' )
328345 url = person .get ('homepage' )
346+ # FIXME: this got cargoculted from npm package.json parsing
329347 yield name and name .strip (), email and email .strip ('<> ' ), url and url .strip ('() ' )
330348 else :
331349 raise Exception ('Incorrect PHP composer composer.json person: %(person)r' % locals ())
0 commit comments