Skip to content

Commit 7d12968

Browse files
authored
Merge pull request #777 from nexB/776-composer-package-detection
Ensure that PHP Composer packages are recognized
2 parents 6b93456 + 6cc7aab commit 7d12968

File tree

15 files changed

+1226
-474
lines changed

15 files changed

+1226
-474
lines changed

src/packagedcode/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
from packagedcode import maven
2727
from packagedcode import npm
2828
from packagedcode import nuget
29+
from packagedcode import phpcomposer
2930
from packagedcode import rpm
3031

3132

3233
# Note: the order matters: from the most to the least specific
34+
# Package classes MUST be added to this list to be active
3335
PACKAGE_TYPES = [
3436
rpm.RpmPackage,
3537
models.DebianPackage,
@@ -43,8 +45,9 @@
4345
models.Axis2Mar,
4446

4547
npm.NpmPackage,
46-
models.BowerPackage,
48+
phpcomposer.PHPComposerPackage,
4749
models.MeteorPackage,
50+
models.BowerPackage,
4851
models.CpanModule,
4952
models.RubyGem,
5053
models.AndroidApp,

src/packagedcode/phpcomposer.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -50,9 +50,9 @@
5050

5151

5252
class 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

195212
def 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())

src/packagedcode/recognize.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
from packagedcode import PACKAGE_TYPES
3434
from commoncode.system import on_linux
35-
from commoncode.fileutils import path_to_unicode
3635
from commoncode.fileutils import path_to_bytes
3736

3837

@@ -74,7 +73,6 @@ def recognize_package(location):
7473
else:
7574
mime_matched = False
7675

77-
7876
extensions = package.extensions
7977
if extensions:
8078
if on_linux:
Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,91 @@
11
{
2-
"type": "phpcomposer",
3-
"name": "jandreasn/a-timer",
4-
"version": null,
5-
"primary_language": "PHP",
6-
"packaging": null,
7-
"summary": "A simple timer.",
8-
"description": null,
9-
"payload_type": null,
10-
"size": null,
11-
"release_date": null,
2+
"type": "phpcomposer",
3+
"name": "jandreasn/a-timer",
4+
"version": null,
5+
"primary_language": "PHP",
6+
"packaging": null,
7+
"summary": "A simple timer.",
8+
"description": null,
9+
"payload_type": null,
10+
"size": null,
11+
"release_date": null,
1212
"authors": [
1313
{
14-
"type": "person",
15-
"name": "Andreas Nilsson",
16-
"email": null,
14+
"type": "person",
15+
"name": "Andreas Nilsson",
16+
"email": null,
1717
"url": null
1818
}
19-
],
20-
"maintainers": [],
21-
"contributors": [],
22-
"owners": [],
23-
"packagers": [],
24-
"distributors": [],
19+
],
20+
"maintainers": [],
21+
"contributors": [],
22+
"owners": [],
23+
"packagers": [],
24+
"distributors": [],
2525
"vendors": [
2626
{
27-
"type": null,
28-
"name": "jandreasn",
29-
"email": null,
27+
"type": null,
28+
"name": "jandreasn",
29+
"email": null,
3030
"url": null
3131
}
32-
],
32+
],
3333
"keywords": [
34-
"timer",
35-
"timing",
34+
"timer",
35+
"timing",
3636
"benchmark"
37-
],
38-
"keywords_doc_url": null,
37+
],
38+
"keywords_doc_url": null,
3939
"metafile_locations": [
4040
"composer.json"
41-
],
42-
"metafile_urls": [],
43-
"homepage_url": "http://github.com/jandreasn/a-timer",
44-
"notes": null,
45-
"download_urls": [],
46-
"download_sha1": null,
47-
"download_sha256": null,
48-
"download_md5": null,
49-
"bug_tracking_url": null,
50-
"support_contacts": [],
51-
"code_view_url": null,
52-
"vcs_tool": null,
53-
"vcs_repository": null,
54-
"vcs_revision": null,
55-
"copyright_top_level": null,
56-
"copyrights": [],
41+
],
42+
"metafile_urls": [],
43+
"homepage_url": "http://github.com/jandreasn/a-timer",
44+
"notes": null,
45+
"download_urls": [],
46+
"download_sha1": null,
47+
"download_sha256": null,
48+
"download_md5": null,
49+
"bug_tracking_url": null,
50+
"support_contacts": [],
51+
"code_view_url": null,
52+
"vcs_tool": null,
53+
"vcs_repository": null,
54+
"vcs_revision": null,
55+
"copyright_top_level": null,
56+
"copyrights": [],
5757
"asserted_licenses": [
5858
{
59-
"license": "MIT",
60-
"url": null,
61-
"text": null,
59+
"license": "MIT",
60+
"url": null,
61+
"text": null,
6262
"notice": null
6363
}
64-
],
65-
"legal_file_locations": [],
66-
"license_expression": null,
67-
"license_texts": [],
68-
"notice_texts": [],
64+
],
65+
"legal_file_locations": [],
66+
"license_expression": null,
67+
"license_texts": [],
68+
"notice_texts": [],
6969
"dependencies": {
7070
"development": [
7171
{
72-
"name": "phpunit/phpunit",
73-
"version": null,
72+
"name": "phpunit/phpunit",
73+
"version": null,
7474
"version_constraint": "^5.6"
75-
},
75+
},
7676
{
77-
"name": "squizlabs/php_codesniffer",
78-
"version": null,
77+
"name": "squizlabs/php_codesniffer",
78+
"version": null,
7979
"version_constraint": "^2.7"
8080
}
81-
],
81+
],
8282
"runtime": [
8383
{
84-
"name": "php",
85-
"version": null,
84+
"name": "php",
85+
"version": null,
8686
"version_constraint": ">=5.6.0"
8787
}
8888
]
89-
},
89+
},
9090
"related_packages": []
9191
}

0 commit comments

Comments
 (0)