Skip to content

Commit acb0e86

Browse files
committed
chore:SP-3589 Enhanced the match summary with clickable links and improved formatting
1 parent 1e03685 commit acb0e86

File tree

5 files changed

+144
-80
lines changed

5 files changed

+144
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
## [1.38.0] - 2025-10-23
1313
### Added
14-
- Added `glc-codequality` format to convert subcomand
14+
- Added `glc-codequality` format to convert subcommand
1515
- Added `inspect gitlab matches` subcommand to generate GitLab-compatible Markdown match summary from SCANOSS scan results
1616
- Added utility modules for shared functionality (`markdown_utils.py` and `file_utils.py`)
1717
### Changed

src/scanoss/gitlabqualityreport.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1+
"""
2+
SPDX-License-Identifier: MIT
3+
4+
Copyright (c) 2025, SCANOSS
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
"""
24+
125
import json
226
import os
327
import sys
428
from dataclasses import dataclass
529

630
from .scanossbase import ScanossBase
31+
from .utils import scanoss_scan_results_utils
732

833

934
@dataclass
@@ -38,17 +63,6 @@ def to_dict(self):
3863
}
3964
}
4065

41-
42-
def _get_lines(lines: str) -> list:
43-
lineArray = []
44-
lines = lines.split(',')
45-
for line in lines:
46-
line_parts = line.split('-')
47-
for part in line_parts:
48-
lineArray.append(int(part))
49-
return lineArray
50-
51-
5266
class GitLabQualityReport(ScanossBase):
5367
"""
5468
GitLabCodeQuality management class
@@ -63,14 +77,14 @@ def __init__(self, debug: bool = False, output_file: str = None):
6377
self.output_file = output_file
6478
self.debug = debug
6579

66-
def _get_code_quality(self, file_name: str,result: dict) -> CodeQuality or None:
80+
def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None:
6781
if not result.get('file_hash'):
6882
self.print_debug(f"Warning: no hash found for result: {result}")
6983
return None
7084
if not result.get('lines') :
7185
self.print_debug(f"Warning: No lines found for result: {result}")
7286
return None
73-
lines = _get_lines(result.get('lines'))
87+
lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
7488
if len(lines) == 0:
7589
self.print_debug(f"Warning: empty lines for result: {result}")
7690
return None
@@ -139,7 +153,7 @@ def _produce_from_str(self, json_str: str, output_file: str = None) -> bool:
139153

140154
def produce_from_file(self, json_file: str, output_file: str = None) -> bool:
141155
"""
142-
Parse plain/raw input JSON file and produce CSV output
156+
Parse plain/raw input JSON file and produce GitLab Code Quality JSON output
143157
:param json_file:
144158
:param output_file:
145159
:return: True if successful, False otherwise

src/scanoss/inspection/raw/match_summary.py

Lines changed: 73 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from dataclasses import dataclass
2626

2727
from ...scanossbase import ScanossBase
28+
from ...utils import scanoss_scan_results_utils
2829
from ..utils.file_utils import load_json_file
2930
from ..utils.markdown_utils import generate_table
3031

@@ -38,10 +39,12 @@ class MatchSummaryItem:
3839
match found during scanning, including file location, license details, and
3940
match quality metrics.
4041
"""
42+
file: str
4143
file_url: str
4244
license: str
4345
similarity: str
4446
purl: str
47+
purl_url: str
4548
version: str
4649
lines: str
4750

@@ -92,25 +95,6 @@ def __init__( # noqa: PLR0913
9295
self.line_range_prefix = line_range_prefix
9396
self.output = output
9497

95-
@staticmethod
96-
def _get_lines(lines: str) -> list:
97-
"""
98-
Parse line range string into a list of line numbers.
99-
100-
Converts SCANOSS line notation (e.g., '10-20,25-30') into a flat list
101-
of individual line numbers for processing.
102-
103-
:param lines: Comma-separated line ranges in SCANOSS format (e.g., '10-20,25-30')
104-
:return: Flat list of all line numbers extracted from the ranges
105-
"""
106-
lineArray = []
107-
lines = lines.split(',')
108-
for line in lines:
109-
line_parts = line.split('-')
110-
for part in line_parts:
111-
lineArray.append(int(part))
112-
return lineArray
113-
11498

11599
def _get_match_summary_item(self, file_name: str, result: dict) -> MatchSummaryItem:
116100
"""
@@ -126,27 +110,64 @@ def _get_match_summary_item(self, file_name: str, result: dict) -> MatchSummaryI
126110
"""
127111
if result.get('id') == "snippet":
128112
# Snippet match: create URL with line range anchor
129-
lines = self._get_lines(result.get('lines'))
113+
lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
130114
end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
131115
file_url = f"{self.line_range_prefix}/{file_name}#L{lines[0]}-L{end_line}"
132116
return MatchSummaryItem(
133117
file_url=file_url,
118+
file=file_name,
134119
license=result.get('licenses')[0].get('name'),
135120
similarity=result.get('matched'),
136121
purl=result.get('purl')[0],
122+
purl_url=result.get('url'),
137123
version=result.get('version'),
138124
lines=f"{lines[0]}-{lines[len(lines) - 1] if len(lines) > 1 else lines[0]}"
139125
)
140126
# File match: create URL without line range
141127
return MatchSummaryItem(
128+
file=file_name,
142129
file_url=f"{self.line_range_prefix}/{file_name}",
143130
license=result.get('licenses')[0].get('name'),
144131
similarity=result.get('matched'),
145132
purl=result.get('purl')[0],
133+
purl_url=result.get('url'),
146134
version=result.get('version'),
147135
lines="all"
148136
)
149137

138+
def _validate_result(self, file_name: str, result: dict) -> bool:
139+
"""
140+
Validate that a scan result has all required fields.
141+
142+
:param file_name: Name of the file being validated
143+
:param result: The scan result to validate
144+
:return: True if valid, False otherwise
145+
"""
146+
validations = [
147+
('id', 'No id found'),
148+
('lines', 'No lines found'),
149+
('purl', 'No purl found'),
150+
('licenses', 'No licenses found'),
151+
('version', 'No version found'),
152+
('matched', 'No matched found'),
153+
('url', 'No url found'),
154+
]
155+
156+
for field, error_msg in validations:
157+
if not result.get(field):
158+
self.print_debug(f'ERROR: {error_msg} for file {file_name}')
159+
return False
160+
161+
# Additional validation for non-empty lists
162+
if len(result.get('purl')) == 0:
163+
self.print_debug(f'ERROR: No purl found for file {file_name}')
164+
return False
165+
if len(result.get('licenses')) == 0:
166+
self.print_debug(f'ERROR: Empty licenses list for file {file_name}')
167+
return False
168+
169+
return True
170+
150171
def _get_matches_summary(self) -> ComponentMatchSummary:
151172
"""
152173
Parse SCANOSS scan results and create categorized match summaries.
@@ -162,29 +183,12 @@ def _get_matches_summary(self) -> ComponentMatchSummary:
162183
# Process each file and its results
163184
for file_name, results in scan_results.items():
164185
for result in results:
165-
# Validate required fields - skip invalid results with debug messages
166-
if not result.get('id'):
167-
self.print_debug(f'ERROR: No id found for file {file_name}')
168-
continue
169-
if result.get('id') == "none": # Skip non-matches
186+
# Skip non-matches
187+
if result.get('id') == "none":
170188
continue
171-
if not result.get('lines'):
172-
self.print_debug(f'ERROR: No lines found for file {file_name}')
173-
continue
174-
if not result.get('purl'):
175-
self.print_debug(f'ERROR: No purl found for file {file_name}')
176-
continue
177-
if not len(result.get('purl')) > 0:
178-
self.print_debug(f'ERROR: No purl found for file {file_name}')
179-
continue
180-
if not result.get('licenses'):
181-
self.print_debug(f'ERROR: No licenses found for file {file_name}')
182-
continue
183-
if not result.get('version'):
184-
self.print_debug(f'ERROR: No version found for file {file_name}')
185-
continue
186-
if not result.get('matched'):
187-
self.print_debug(f'ERROR: No matched found for file {file_name}')
189+
190+
# Validate required fields
191+
if not self._validate_result(file_name, result):
188192
continue
189193

190194
# Create summary item and categorize by match type
@@ -207,50 +211,53 @@ def _markdown(self, gitlab_matches_summary: ComponentMatchSummary) -> str:
207211
:param gitlab_matches_summary: Container with categorized file and snippet matches to format
208212
:return: Complete Markdown document with formatted match tables
209213
"""
210-
# Define table headers
211-
headers = ['File', 'License', 'Similarity', 'PURL', 'Version', 'Lines']
212214

215+
if len(gitlab_matches_summary.files) == 0 and len(gitlab_matches_summary.snippet) == 0:
216+
return ""
217+
218+
# Define table headers
219+
file_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version']
220+
snippet_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version', 'Lines']
213221
# Build file matches table
214222
file_match_rows = []
215-
for file in gitlab_matches_summary.files:
223+
for file_match in gitlab_matches_summary.files:
216224
row = [
217-
file.file_url,
218-
file.license,
219-
file.similarity,
220-
file.purl,
221-
file.version,
222-
file.lines
225+
f"[{file_match.file}]({file_match.file_url})",
226+
file_match.license,
227+
file_match.similarity,
228+
f"[{file_match.purl}]({file_match.purl_url})",
229+
file_match.version,
223230
]
224231
file_match_rows.append(row)
225-
file_match_table = generate_table(headers, file_match_rows)
232+
file_match_table = generate_table(file_match_headers, file_match_rows)
226233

227234
# Build snippet matches table
228235
snippet_match_rows = []
229-
for file in gitlab_matches_summary.snippet:
236+
for snippet_match in gitlab_matches_summary.snippet:
230237
row = [
231-
file.file_url,
232-
file.license,
233-
file.similarity,
234-
file.purl,
235-
file.version,
236-
file.lines
238+
f"[{snippet_match.file}]({snippet_match.file_url})",
239+
snippet_match.license,
240+
snippet_match.similarity,
241+
f"[{snippet_match.purl}]({snippet_match.purl_url})",
242+
snippet_match.version,
243+
snippet_match.lines
237244
]
238245
snippet_match_rows.append(row)
239-
snippet_match_table = generate_table(headers, snippet_match_rows)
246+
snippet_match_table = generate_table(snippet_match_headers, snippet_match_rows)
240247

241248
# Assemble complete Markdown document
242249
markdown = ""
243-
markdown += "### SCANOSS Matches Summary\n\n"
250+
markdown += "### SCANOSS Match Summary\n\n"
244251

245252
# File matches section (collapsible)
246253
markdown += "<details>\n"
247-
markdown += "<summary>File Matches Summary</summary>\n\n"
254+
markdown += "<summary>File Match Summary</summary>\n\n"
248255
markdown += file_match_table
249256
markdown += "\n</details>\n"
250257

251258
# Snippet matches section (collapsible)
252259
markdown += "<details>\n"
253-
markdown += "<summary>Snippet Matches Summary</summary>\n\n"
260+
markdown += "<summary>Snippet Match Summary</summary>\n\n"
254261
markdown += snippet_match_table
255262
markdown += "\n</details>\n"
256263

@@ -272,7 +279,9 @@ def run(self):
272279

273280
# Format matches as GitLab-compatible Markdown
274281
matches_md = self._markdown(matches)
275-
282+
if matches_md == "":
283+
self.print_stdout("No matches found.")
284+
return
276285
# Output to file or stdout
277286
self.print_to_file_or_stdout(matches_md, self.output)
278287

src/scanoss/inspection/utils/file_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import os
2727

2828

29-
def load_json_file(file_path: str) -> dict or Exception:
29+
def load_json_file(file_path: str) -> dict:
3030
"""
3131
Load the file
3232
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
SPDX-License-Identifier: MIT
3+
4+
Copyright (c) 2025, SCANOSS
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
"""
24+
25+
def get_lines(lines: str) -> list:
26+
"""
27+
Parse line range string into a list of line numbers.
28+
29+
Converts SCANOSS line notation (e.g., '10-20,25-30') into a flat list
30+
of individual line numbers for processing.
31+
32+
:param lines: Comma-separated line ranges in SCANOSS format (e.g., '10-20,25-30')
33+
:return: Flat list of all line numbers extracted from the ranges
34+
"""
35+
lines_list = []
36+
lines = lines.split(',')
37+
for line in lines:
38+
line_parts = line.split('-')
39+
for part in line_parts:
40+
lines_list.append(int(part))
41+
return lines_list

0 commit comments

Comments
 (0)