Skip to content

Commit d9cf35b

Browse files
Support glob patterns in referenced license files
Signed-off-by: Sidheshwar Sarangal <sidheshwar.sarangal@gmail.com>
1 parent 058f439 commit d9cf35b

7 files changed

Lines changed: 109 additions & 2 deletions

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The following organizations or individuals have contributed to ScanCode:
8787
- Shrijal Acharya @OctoPie23
8888
- Shivam Chauhan @chashiv
8989
- Shivam Sandbhor @sbs2001
90+
- Sidheshwar Sarangal @SidheshwarSarangal
9091
- Steven Esser @majurg
9192
- Sushant Gupta @susg
9293
- Theodore Aptekarev @piiq

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Changelog
44
Next release
55
--------------
66

7+
- Support directory glob patterns in license rule ``referenced_filenames``.
8+
https://github.com/aboutcode-org/scancode-toolkit/issues/4276
9+
710
- Fix the optional ``licenses`` extra dependency typo to install
811
``licensedcode-data``.
912
https://github.com/aboutcode-org/scancode-toolkit/pull/5056

src/licensedcode/detection.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import uuid
1616
from enum import Enum
1717
from hashlib import sha1
18+
from fnmatch import fnmatchcase
1819

1920
import attr
2021
from collections import defaultdict
@@ -1990,6 +1991,73 @@ def find_referenced_resource(referenced_filename, resource, codebase, **kwargs):
19901991
return resource
19911992

19921993

1994+
def find_referenced_resources(
1995+
referenced_filename,
1996+
resource,
1997+
codebase,
1998+
find_referenced_resource_func,
1999+
):
2000+
"""
2001+
Return a list of Resources matching ``referenced_filename`` for ``resource``
2002+
in ``codebase``. ``referenced_filename`` can be an exact path or a glob
2003+
pattern.
2004+
2005+
Exact paths use ``find_referenced_resource_func`` directly. Glob patterns
2006+
are matched one path segment at a time so that wildcards do not cross
2007+
directory boundaries. Each match is resolved again with
2008+
``find_referenced_resource_func`` to retain its existing location rules.
2009+
"""
2010+
if not (referenced_filename and resource):
2011+
return []
2012+
2013+
referenced_filename = clean_path(referenced_filename)
2014+
if not any(character in referenced_filename for character in '*?['):
2015+
referenced_resource = find_referenced_resource_func(
2016+
referenced_filename=referenced_filename,
2017+
resource=resource,
2018+
codebase=codebase,
2019+
)
2020+
return [referenced_resource] if referenced_resource else []
2021+
2022+
pattern_parts = referenced_filename.split('/')
2023+
referenced_resources = []
2024+
seen_paths = set()
2025+
2026+
for candidate in codebase.walk(skip_root=True):
2027+
candidate_path = as_posixpath(candidate.path)
2028+
candidate_parts = candidate_path.split('/')
2029+
if len(candidate_parts) < len(pattern_parts):
2030+
continue
2031+
2032+
candidate_reference_parts = candidate_parts[-len(pattern_parts):]
2033+
if not all(
2034+
fnmatchcase(candidate_part, pattern_part)
2035+
for candidate_part, pattern_part in zip(
2036+
candidate_reference_parts,
2037+
pattern_parts,
2038+
)
2039+
):
2040+
continue
2041+
2042+
candidate_reference = '/'.join(candidate_reference_parts)
2043+
referenced_resource = find_referenced_resource_func(
2044+
referenced_filename=candidate_reference,
2045+
resource=resource,
2046+
codebase=codebase,
2047+
)
2048+
if not referenced_resource:
2049+
continue
2050+
2051+
referenced_path = as_posixpath(referenced_resource.path)
2052+
if referenced_path != candidate_path or referenced_path in seen_paths:
2053+
continue
2054+
2055+
seen_paths.add(referenced_path)
2056+
referenced_resources.append(referenced_resource)
2057+
2058+
return referenced_resources
2059+
2060+
19932061
def update_expressions_from_license_detections(resource, codebase):
19942062
"""
19952063
Set the `detected_license_expression` and `detected_license_expression_spdx`
@@ -2046,14 +2114,23 @@ def update_detection_from_referenced_files(
20462114

20472115
referenced_detections = []
20482116
referenced_resources = []
2117+
seen_referenced_resource_paths = set()
20492118
for referenced_filename in referenced_filenames:
2050-
referenced_resource = find_referenced_resource_func(
2119+
resolved_resources = find_referenced_resources(
20512120
referenced_filename=referenced_filename,
20522121
resource=resource,
20532122
codebase=codebase,
2123+
find_referenced_resource_func=find_referenced_resource_func,
20542124
)
20552125

2056-
if referenced_resource and referenced_resource.license_detections:
2126+
for referenced_resource in resolved_resources:
2127+
if (
2128+
referenced_resource.path in seen_referenced_resource_paths
2129+
or not referenced_resource.license_detections
2130+
):
2131+
continue
2132+
2133+
seen_referenced_resource_paths.add(referenced_resource.path)
20572134
referenced_detections.extend(
20582135
referenced_resource.license_detections
20592136
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See the license files in licenses/*.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Apache License, Version 2.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MIT License

tests/licensedcode/test_plugin_license_detection.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from commoncode.testcase import FileDrivenTesting
1313

14+
from licensedcode.detection import find_referenced_resources
1415
from licensedcode.plugin_license import find_referenced_resource
1516
from scancode.cli_test_utils import check_json_scan
1617
from scancode.cli_test_utils import run_scan_click
@@ -367,6 +368,28 @@ def test_find_referenced_resource_does_not_find_based_file_name_suffix():
367368
assert result.path == 'scan-ref-dupe-name-suffix/LICENSE'
368369

369370

371+
def test_find_referenced_resources_with_directory_glob():
372+
test_dir = test_env.get_test_loc('plugin_license/license_reference/scan/scan-ref-glob')
373+
scan_loc = test_env.get_temp_file('json')
374+
args = ['--license', '--json', scan_loc, test_dir]
375+
run_scan_click(args)
376+
377+
from commoncode.resource import VirtualCodebase
378+
codebase = VirtualCodebase(scan_loc)
379+
resource = codebase.get_resource(path='scan-ref-glob/license-notice.txt')
380+
results = find_referenced_resources(
381+
referenced_filename='licenses/*',
382+
resource=resource,
383+
codebase=codebase,
384+
find_referenced_resource_func=find_referenced_resource,
385+
)
386+
387+
assert [result.path for result in results] == [
388+
'scan-ref-glob/licenses/COPYING',
389+
'scan-ref-glob/licenses/LICENSE',
390+
]
391+
392+
370393
def test_match_reference_license():
371394
# Setup: Create a new scan to use for a virtual codebase
372395
test_dir = test_env.get_test_loc('plugin_license/license_reference/scan/scan-ref')

0 commit comments

Comments
 (0)