Skip to content

Commit a418217

Browse files
DevonLcolinmoynes
andauthored
fix: unify license parsing around Cloudsmith SPDX metadata (#30)
* fix: unify license parsing around Cloudsmith SPDX metadata - Use Cloudsmith SPDX metadata as canonical license source - Preserve Cloudsmith-derived license text across all package views - Align badges, tooltips, filters, and search on shared classification path - Fix restrictive license queries to use shared license rules - Resolve View License consistently from shared metadata including SPDX-only packages * fix: reuse precomputed search query in license quick-pick items - Carry searchQuery from inspection through to quick-pick items - Update _inspectStringLicense JSDoc to match actual return shape --------- Co-authored-by: Colin Moynes <77994705+colinmoynes@users.noreply.github.com>
1 parent 4f71bc8 commit a418217

8 files changed

Lines changed: 913 additions & 91 deletions

extension.js

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { UpstreamDetailProvider } = require("./views/upstreamDetailProvider");
1818
const { PromotionProvider } = require("./views/promotionProvider");
1919
const { SearchQueryBuilder } = require("./util/searchQueryBuilder");
2020
const { formatApiError } = require("./util/errorFormatter");
21+
const { LicenseClassifier } = require("./util/licenseClassifier");
2122
const recentPackages = require("./util/recentPackages");
2223

2324
/**
@@ -218,8 +219,8 @@ const FILTER_PRESETS = [
218219
applyBuilder: (builder) => builder.raw("license_policy_violated:true"),
219220
},
220221
{
221-
label: "Packages with restrictive licenses (AGPL, GPL, SSPL)",
222-
applyBuilder: (builder) => builder.raw("(license:AGPL OR license:GPL OR license:SSPL)"),
222+
label: "Packages with restrictive licenses",
223+
applyBuilder: (builder) => builder.raw(LicenseClassifier.buildRestrictiveQuery()),
223224
},
224225
{
225226
label: "Custom query",
@@ -1565,21 +1566,7 @@ async function activate(context) {
15651566
}
15661567

15671568
// Select license tier/type
1568-
const licenseItems = [
1569-
{ label: "Restrictive", kind: vscode.QuickPickItemKind.Separator },
1570-
{ label: "AGPL-3.0", description: "Strong copyleft" },
1571-
{ label: "GPL-3.0", description: "Strong copyleft" },
1572-
{ label: "GPL-2.0", description: "Strong copyleft" },
1573-
{ label: "SSPL-1.0", description: "Server-side copyleft" },
1574-
{ label: "Cautious", kind: vscode.QuickPickItemKind.Separator },
1575-
{ label: "LGPL-3.0", description: "Weak copyleft" },
1576-
{ label: "MPL-2.0", description: "Weak copyleft" },
1577-
{ label: "EPL-2.0", description: "Weak copyleft" },
1578-
{ label: "Permissive", kind: vscode.QuickPickItemKind.Separator },
1579-
{ label: "MIT", description: "Permissive" },
1580-
{ label: "Apache-2.0", description: "Permissive" },
1581-
{ label: "BSD-3-Clause", description: "Permissive" },
1582-
];
1569+
const licenseItems = LicenseClassifier.getSearchQuickPickItems();
15831570

15841571
const selectedLicense = await vscode.window.showQuickPick(licenseItems, {
15851572
placeHolder: "Select a license to search for",
@@ -1588,21 +1575,24 @@ async function activate(context) {
15881575
return;
15891576
}
15901577

1591-
const query = `license:${selectedLicense.label}`;
1578+
const query = selectedLicense.query || LicenseClassifier.buildLicenseQuery(selectedLicense.label);
15921579
const recentSearches = new RecentSearches(context, workspaceSlug);
15931580
recentSearches.add({ workspace: workspaceSlug, query: query, scope: "workspace" });
15941581
await searchProvider.search(workspaceSlug, query);
15951582
}),
15961583

15971584
// Register open license URL command
15981585
vscode.commands.registerCommand("cloudsmith-vsc.openLicenseUrl", async (item) => {
1599-
if (!item || !item.licenseUrl) {
1586+
const licenseInfo = item && item.licenseInfo ? item.licenseInfo : LicenseClassifier.inspect(item);
1587+
const licenseUrl = licenseInfo ? (licenseInfo.licenseUrl || (item && item.licenseUrl) || null) : null;
1588+
1589+
if (!item || !licenseUrl) {
16001590
vscode.window.showWarningMessage("No license URL available.");
16011591
return;
16021592
}
16031593
let parsedUrl;
16041594
try {
1605-
parsedUrl = new URL(item.licenseUrl);
1595+
parsedUrl = new URL(licenseUrl);
16061596
} catch (err) { // eslint-disable-line no-unused-vars
16071597
vscode.window.showWarningMessage("Invalid license URL.");
16081598
return;

models/dependencyHealthNode.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// cross-referenced against Cloudsmith
33

44
const vscode = require("vscode");
5+
const { LicenseClassifier } = require("../util/licenseClassifier");
56

67
class DependencyHealthNode {
78
/**
@@ -40,6 +41,11 @@ class DependencyHealthNode {
4041
this.num_vulnerabilities = cloudsmithMatch.num_vulnerabilities || 0;
4142
this.max_severity = cloudsmithMatch.max_severity || null;
4243
this.status_reason = cloudsmithMatch.status_reason || null;
44+
this.licenseInfo = LicenseClassifier.inspect(cloudsmithMatch);
45+
this.spdx_license = this.licenseInfo.spdxLicense;
46+
this.raw_license = this.licenseInfo.rawLicense;
47+
this.license = this.licenseInfo.displayValue;
48+
this.license_url = this.licenseInfo.licenseUrl;
4349
}
4450
}
4551

@@ -132,16 +138,15 @@ class DependencyHealthNode {
132138
if (match.num_vulnerabilities > 0) {
133139
lines.push(`Vulnerabilities: ${match.num_vulnerabilities} (${match.max_severity || "Unknown"})`);
134140
}
135-
if (match.license) {
136-
const { LicenseClassifier } = require("../util/licenseClassifier");
137-
const classification = LicenseClassifier.classify(match.license);
138-
const tierLabels = {
139-
"restrictive": "Restrictive",
140-
"cautious": "Cautious",
141-
"permissive": "Permissive",
142-
"unknown": "Unknown",
143-
};
144-
lines.push(`License: ${match.license} (${tierLabels[classification.tier] || "Unknown"})`);
141+
const classification = this.licenseInfo || LicenseClassifier.inspect(match);
142+
if (classification.displayValue) {
143+
lines.push(`License: ${classification.label} (${classification.metadata.label})`);
144+
if (classification.spdxLicense && classification.spdxLicense !== classification.label) {
145+
lines.push(`Canonical SPDX: ${classification.spdxLicense}`);
146+
}
147+
if (classification.overrideApplied) {
148+
lines.push("License classification includes a local restrictive override.");
149+
}
145150
}
146151

147152
if (this.state === "quarantined" || this.state === "violated") {
@@ -214,9 +219,9 @@ class DependencyHealthNode {
214219

215220
// License with classification
216221
const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
217-
if (config.get("showLicenseIndicators") !== false && match.license) {
222+
if (config.get("showLicenseIndicators") !== false && this.licenseInfo && this.licenseInfo.displayValue) {
218223
const LicenseNode = require("./licenseNode");
219-
children.push(new LicenseNode(match.license, match.license_url || null, this.context));
224+
children.push(new LicenseNode(this.licenseInfo, this.context));
220225
}
221226

222227
// Vulnerability summary

models/licenseNode.js

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@ const { LicenseClassifier } = require("../util/licenseClassifier");
55

66
class LicenseNode {
77
/**
8-
* @param {string|null} license SPDX license identifier.
9-
* @param {string|null} licenseUrl URL to license text.
8+
* @param {Object|string|null} licenseSource Cloudsmith license metadata or license string.
9+
* @param {string|vscode.ExtensionContext|null} licenseUrlOrContext License URL when using the legacy signature, or context when using metadata.
1010
* @param {vscode.ExtensionContext} context
1111
*/
12-
constructor(license, licenseUrl, context) {
13-
this.license = license;
14-
this.licenseUrl = licenseUrl || null;
15-
this.context = context;
16-
this.classification = LicenseClassifier.classify(license);
12+
constructor(licenseSource, licenseUrlOrContext, context) {
13+
if (context === undefined && licenseSource && typeof licenseSource === "object" && !Array.isArray(licenseSource)) {
14+
this.context = licenseUrlOrContext;
15+
this.licenseInfo = LicenseClassifier.inspect(licenseSource);
16+
} else {
17+
this.context = context;
18+
this.licenseInfo = LicenseClassifier.inspect({
19+
license: licenseSource,
20+
license_url: licenseUrlOrContext,
21+
});
22+
}
23+
24+
this.license = this.licenseInfo.displayValue;
25+
this.licenseUrl = this.licenseInfo.licenseUrl || null;
26+
this.classification = this.licenseInfo;
1727
}
1828

1929
_getIcon() {
@@ -27,24 +37,19 @@ class LicenseNode {
2737
}
2838

2939
_getDescription() {
30-
const tierLabel = {
31-
"restrictive": "Restrictive",
32-
"cautious": "Cautious",
33-
"permissive": "Permissive",
34-
"unknown": "Unknown license",
35-
};
36-
return tierLabel[this.classification.tier] || tierLabel["unknown"];
40+
return this.classification.metadata.description;
3741
}
3842

3943
_buildTooltip() {
40-
const tips = {
41-
"restrictive": "This license has strong copyleft or viral terms that may require releasing derivative works under the same license. Legal review recommended before use in commercial software.",
42-
"cautious": "This license has weak copyleft or uncommon terms. Review the specific obligations before use.",
43-
"permissive": "This license is generally compatible with commercial use with minimal obligations.",
44-
"unknown": "This license was not recognized. Review the license text manually.",
45-
};
4644
const licenseLabel = this.license || "Not specified";
47-
return `${licenseLabel}\n\n${tips[this.classification.tier] || tips["unknown"]}`;
45+
const lines = [licenseLabel, "", this.classification.metadata.tooltip];
46+
if (this.classification.spdxLicense && this.classification.spdxLicense !== licenseLabel) {
47+
lines.push("", `Canonical SPDX: ${this.classification.spdxLicense}`);
48+
}
49+
if (this.classification.overrideApplied) {
50+
lines.push("", "Local restrictive override applied via cloudsmith-vsc.restrictiveLicenses.");
51+
}
52+
return lines.join("\n");
4853
}
4954

5055
getTreeItem() {

models/packageNode.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const vscode = require("vscode");
44
const path = require("path");
5+
const { LicenseClassifier } = require("../util/licenseClassifier");
56

67
class PackageNode {
78
constructor(pkg, context) {
@@ -57,8 +58,11 @@ class PackageNode {
5758
}
5859

5960
// License fields from API response (may be absent in list endpoint)
60-
this.license = pkg.license || null;
61-
this.license_url = pkg.license_url || null;
61+
this.licenseInfo = LicenseClassifier.inspect(pkg);
62+
this.spdx_license = this.licenseInfo.spdxLicense;
63+
this.raw_license = this.licenseInfo.rawLicense;
64+
this.license = this.licenseInfo.displayValue;
65+
this.license_url = this.licenseInfo.licenseUrl;
6266

6367
// Raw tags for upstream origin detection
6468
this.tags_raw = pkg.tags || {};
@@ -205,7 +209,7 @@ class PackageNode {
205209
const showLicense = config.get("showLicenseIndicators") !== false;
206210
if (showLicense) {
207211
const LicenseNode = require("./licenseNode");
208-
children.push(new LicenseNode(this.license, this.license_url, this.context));
212+
children.push(new LicenseNode(this.licenseInfo, this.context));
209213
}
210214

211215
// 4. Vulnerability summary (expandable)

models/searchResultNode.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const vscode = require("vscode");
55
const path = require("path");
66
const PackageDetailsNode = require("./packageDetailsNode");
7+
const { LicenseClassifier } = require("../util/licenseClassifier");
78

89
class SearchResultNode {
910
constructor(pkg, context) {
@@ -60,8 +61,11 @@ class SearchResultNode {
6061
}
6162

6263
// License fields from API response (may be absent in list endpoint)
63-
this.license = pkg.license || null;
64-
this.license_url = pkg.license_url || null;
64+
this.licenseInfo = LicenseClassifier.inspect(pkg);
65+
this.spdx_license = this.licenseInfo.spdxLicense;
66+
this.raw_license = this.licenseInfo.rawLicense;
67+
this.license = this.licenseInfo.displayValue;
68+
this.license_url = this.licenseInfo.licenseUrl;
6569

6670
// Raw tags for upstream origin detection
6771
this.tags_raw = pkg.tags || {};
@@ -201,7 +205,7 @@ class SearchResultNode {
201205
const showLicense = config.get("showLicenseIndicators") !== false;
202206
if (showLicense) {
203207
const LicenseNode = require("./licenseNode");
204-
children.push(new LicenseNode(this.license, this.license_url, this.context));
208+
children.push(new LicenseNode(this.licenseInfo, this.context));
205209
}
206210

207211
// 4. Vulnerability summary (expandable)

0 commit comments

Comments
 (0)