@callstack/licenses 0.3.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/build/constants/licenses.d.ts +4 -0
- package/build/constants/licenses.js +15 -15
- package/build/licenses/OrPolicy.d.ts +21 -0
- package/build/licenses/OrPolicy.js +16 -0
- package/build/licenses/index.d.ts +3 -0
- package/build/licenses/index.js +3 -0
- package/build/licenses/licenseAnalysis.d.ts +7 -8
- package/build/licenses/licenseAnalysis.js +15 -29
- package/build/licenses/licenseClassification.d.ts +52 -0
- package/build/licenses/licenseClassification.js +136 -0
- package/build/licenses/licenseExpression.d.ts +37 -0
- package/build/licenses/licenseExpression.js +166 -0
- package/build/node/common.d.ts +23 -2
- package/build/node/common.js +159 -56
- package/build/node/index.d.ts +1 -0
- package/build/node/index.js +1 -0
- package/build/node/mergeLicensesMappings.d.ts +8 -0
- package/build/node/mergeLicensesMappings.js +28 -0
- package/build/node/utils/aboutLibrariesLicenses.d.ts +11 -0
- package/build/node/utils/aboutLibrariesLicenses.js +58 -0
- package/build/node/utils/licenseFiles.d.ts +8 -0
- package/build/node/utils/licenseFiles.js +45 -0
- package/build/node/utils/licensePlistBody.d.ts +6 -0
- package/build/node/utils/licensePlistBody.js +20 -0
- package/build/node/utils/packageUtils.d.ts +14 -5
- package/build/node/utils/packageUtils.js +35 -23
- package/build/types/AboutLibrariesLikePackageInfo.d.ts +1 -1
- package/build/types/License.d.ts +14 -6
- package/build/types/LicenseAnalysisResult.d.ts +7 -0
- package/build/types/LicenseExpression.d.ts +39 -0
- package/build/types/LicenseExpression.js +2 -0
- package/build/types/LicenseFile.d.ts +8 -0
- package/build/types/LicenseFile.js +2 -0
- package/build/types/LicenseStats.d.ts +1 -1
- package/build/types/ScanDependenciesOptions.d.ts +12 -0
- package/build/types/ScanDependenciesOptions.js +2 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.js +3 -0
- package/package.json +9 -6
- package/src/constants/licenses.ts +15 -15
- package/src/licenses/OrPolicy.ts +23 -0
- package/src/licenses/index.ts +3 -0
- package/src/licenses/licenseAnalysis.ts +19 -33
- package/src/licenses/licenseClassification.ts +173 -0
- package/src/licenses/licenseExpression.ts +200 -0
- package/src/node/common.ts +217 -66
- package/src/node/index.ts +1 -0
- package/src/node/mergeLicensesMappings.ts +32 -0
- package/src/node/utils/aboutLibrariesLicenses.ts +69 -0
- package/src/node/utils/licenseFiles.ts +50 -0
- package/src/node/utils/licensePlistBody.ts +18 -0
- package/src/node/utils/packageUtils.ts +37 -26
- package/src/types/AboutLibrariesLikePackageInfo.ts +1 -1
- package/src/types/License.ts +16 -7
- package/src/types/LicenseAnalysisResult.ts +8 -0
- package/src/types/LicenseExpression.ts +22 -0
- package/src/types/LicenseFile.ts +10 -0
- package/src/types/LicenseStats.ts +1 -1
- package/src/types/ScanDependenciesOptions.ts +12 -0
- package/src/types/index.ts +3 -0
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
Types,
|
|
34
34
|
generateAboutLibrariesNPMOutput,
|
|
35
35
|
generateLicensePlistNPMOutput,
|
|
36
|
+
renderLicenseExpression,
|
|
36
37
|
scanDependencies,
|
|
37
38
|
} from '@callstack/licenses';
|
|
38
39
|
import * as md from 'ts-markdown-builder';
|
|
@@ -58,17 +59,16 @@ const licensePlistReport = generateLicensePlistNPMOutput(licenses, iosProjectPat
|
|
|
58
59
|
const markdownString = md
|
|
59
60
|
.joinBlocks(
|
|
60
61
|
Object.entries(licenses)
|
|
61
|
-
.flatMap(([packageKey, { name: packageName, version, author,
|
|
62
|
+
.flatMap(([packageKey, { name: packageName, version, author, description, license, licenseFiles, url }]) => [
|
|
62
63
|
md.heading(packageName, { level: 2 }),
|
|
63
64
|
'\n',
|
|
64
65
|
`Version: ${version}<br/>\n`,
|
|
65
66
|
url ? `URL: ${url}<br/>\n` : '',
|
|
66
67
|
author ? `Author: ${author}<br/>\n\n` : '',
|
|
67
|
-
content ?? '',
|
|
68
|
+
licenseFiles.map(({ licenseId, file, content }) => `${licenseId ?? file}:\n\n${content}`).join('\n\n'),
|
|
68
69
|
'\n',
|
|
69
70
|
description ? `Description: ${description}\n` : '',
|
|
70
|
-
|
|
71
|
-
type ? `Type: ${type}` : '',
|
|
71
|
+
`License: ${renderLicenseExpression(license)}`,
|
|
72
72
|
'\n',
|
|
73
73
|
md.horizontalRule,
|
|
74
74
|
])
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* https://spdx.org/licenses
|
|
3
|
+
*
|
|
4
|
+
* Canonical License Identifiers, as produced by parsing a License Expression: deprecated identifiers are upgraded
|
|
5
|
+
* (`GPL-2.0` becomes `GPL-2.0-only`) and `+` is absorbed (`GPL-2.0+` becomes `GPL-2.0-or-later`), so neither form is listed.
|
|
6
|
+
* The exception are deprecated identifiers with a built-in License Exception, which have no `-only` form.
|
|
3
7
|
*/
|
|
4
8
|
export declare const STRONG_COPYLEFT_LICENSES: Set<string>;
|
|
5
9
|
export declare const WEAK_COPYLEFT_LICENSES: Set<string>;
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* https://spdx.org/licenses
|
|
4
|
+
*
|
|
5
|
+
* Canonical License Identifiers, as produced by parsing a License Expression: deprecated identifiers are upgraded
|
|
6
|
+
* (`GPL-2.0` becomes `GPL-2.0-only`) and `+` is absorbed (`GPL-2.0+` becomes `GPL-2.0-or-later`), so neither form is listed.
|
|
7
|
+
* The exception are deprecated identifiers with a built-in License Exception, which have no `-only` form.
|
|
4
8
|
*/
|
|
5
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
10
|
exports.WEAK_COPYLEFT_LICENSES_LOWERCASE = exports.STRONG_COPYLEFT_LICENSES_LOWERCASE = exports.WEAK_COPYLEFT_LICENSES = exports.STRONG_COPYLEFT_LICENSES = void 0;
|
|
7
11
|
exports.STRONG_COPYLEFT_LICENSES = new Set([
|
|
8
|
-
'GPL',
|
|
9
|
-
'GPL-1.0',
|
|
10
|
-
'GPL-1.0+',
|
|
11
|
-
'GPL-2.0',
|
|
12
|
-
'GPL-2.0+',
|
|
12
|
+
'GPL-1.0-only',
|
|
13
|
+
'GPL-1.0-or-later',
|
|
13
14
|
'GPL-2.0-only',
|
|
14
15
|
'GPL-2.0-or-later',
|
|
15
|
-
'GPL-
|
|
16
|
-
'GPL-
|
|
16
|
+
'GPL-2.0-with-autoconf-exception',
|
|
17
|
+
'GPL-2.0-with-bison-exception',
|
|
18
|
+
'GPL-2.0-with-classpath-exception',
|
|
19
|
+
'GPL-2.0-with-font-exception',
|
|
20
|
+
'GPL-2.0-with-GCC-exception',
|
|
21
|
+
'GPL-3.0-with-autoconf-exception',
|
|
22
|
+
'GPL-3.0-with-GCC-exception',
|
|
17
23
|
'GPL-3.0-only',
|
|
18
24
|
'GPL-3.0-or-later',
|
|
19
|
-
'AGPL-
|
|
25
|
+
'AGPL-1.0-only',
|
|
26
|
+
'AGPL-1.0-or-later',
|
|
20
27
|
'AGPL-3.0-only',
|
|
21
28
|
'AGPL-3.0-or-later',
|
|
22
29
|
'EUPL-1.0',
|
|
@@ -33,17 +40,10 @@ exports.WEAK_COPYLEFT_LICENSES = new Set([
|
|
|
33
40
|
'CDDL-1.1',
|
|
34
41
|
'EPL-1.0',
|
|
35
42
|
'EPL-2.0',
|
|
36
|
-
'LGPL',
|
|
37
|
-
'LGPL-2.0',
|
|
38
|
-
'LGPL-2.0+',
|
|
39
43
|
'LGPL-2.0-only',
|
|
40
44
|
'LGPL-2.0-or-later',
|
|
41
|
-
'LGPL-2.1',
|
|
42
|
-
'LGPL-2.1+',
|
|
43
45
|
'LGPL-2.1-only',
|
|
44
46
|
'LGPL-2.1-or-later',
|
|
45
|
-
'LGPL-3.0',
|
|
46
|
-
'LGPL-3.0+',
|
|
47
47
|
'LGPL-3.0-only',
|
|
48
48
|
'LGPL-3.0-or-later',
|
|
49
49
|
'MPL-1.1',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumerates all valid OR Policy values
|
|
3
|
+
*
|
|
4
|
+
* @see {@link OrPolicy}
|
|
5
|
+
*/
|
|
6
|
+
export declare const validOrPolicies: readonly ["most-restrictive", "least-restrictive"];
|
|
7
|
+
/**
|
|
8
|
+
* Rule for which operand of a Dual License (`A OR B`) determines its License Category:
|
|
9
|
+
* - 'most-restrictive' (the most restrictive operand)
|
|
10
|
+
* - 'least-restrictive' (the least restrictive operand; matches SPDX's "choice" meaning of OR)
|
|
11
|
+
*
|
|
12
|
+
* Never applies to AND, which always takes the most restrictive operand.
|
|
13
|
+
*/
|
|
14
|
+
export type OrPolicy = (typeof validOrPolicies)[number];
|
|
15
|
+
/**
|
|
16
|
+
* OR Policy used when none is given: a Dual License takes its most restrictive operand,
|
|
17
|
+
* as a conservative default for compliance checks.
|
|
18
|
+
*
|
|
19
|
+
* @see {@link OrPolicy}
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_OR_POLICY: OrPolicy;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_OR_POLICY = exports.validOrPolicies = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Enumerates all valid OR Policy values
|
|
6
|
+
*
|
|
7
|
+
* @see {@link OrPolicy}
|
|
8
|
+
*/
|
|
9
|
+
exports.validOrPolicies = ['most-restrictive', 'least-restrictive'];
|
|
10
|
+
/**
|
|
11
|
+
* OR Policy used when none is given: a Dual License takes its most restrictive operand,
|
|
12
|
+
* as a conservative default for compliance checks.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link OrPolicy}
|
|
15
|
+
*/
|
|
16
|
+
exports.DEFAULT_OR_POLICY = 'most-restrictive';
|
package/build/licenses/index.js
CHANGED
|
@@ -17,3 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./LicenseCategory"), exports);
|
|
18
18
|
__exportStar(require("./descriptions"), exports);
|
|
19
19
|
__exportStar(require("./licenseAnalysis"), exports);
|
|
20
|
+
__exportStar(require("./OrPolicy"), exports);
|
|
21
|
+
__exportStar(require("./licenseExpression"), exports);
|
|
22
|
+
__exportStar(require("./licenseClassification"), exports);
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { AggregatedLicensesMapping } from '../types';
|
|
2
2
|
import type { LicenseAnalysisResult } from '../types/LicenseAnalysisResult';
|
|
3
|
-
import {
|
|
4
|
-
/**
|
|
5
|
-
* Categorizes a license based on its copyleft characteristics.
|
|
6
|
-
* @param licenseType the license type
|
|
7
|
-
* @returns the license category
|
|
8
|
-
*/
|
|
9
|
-
export declare function categorizeLicense(licenseType?: string): LicenseCategory;
|
|
3
|
+
import type { OrPolicy } from './OrPolicy';
|
|
10
4
|
/**
|
|
11
5
|
* Analyzes license data and returns comprehensive statistics.
|
|
6
|
+
*
|
|
7
|
+
* Each package is classified by its License Expression and grouped by the rendered expression,
|
|
8
|
+
* so a Dual License such as `MIT OR Apache-2.0` is counted under one key.
|
|
9
|
+
*
|
|
12
10
|
* @param report the licenses report data
|
|
11
|
+
* @param orPolicy the OR Policy used to classify Dual Licenses; defaults to {@link DEFAULT_OR_POLICY}
|
|
13
12
|
* @returns the license analysis result
|
|
14
13
|
*/
|
|
15
|
-
export declare function analyzeLicenses(report: AggregatedLicensesMapping): LicenseAnalysisResult;
|
|
14
|
+
export declare function analyzeLicenses(report: AggregatedLicensesMapping, orPolicy?: OrPolicy): LicenseAnalysisResult;
|
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.categorizeLicense = categorizeLicense;
|
|
4
3
|
exports.analyzeLicenses = analyzeLicenses;
|
|
5
|
-
const licenses_1 = require("../constants/licenses");
|
|
6
4
|
const LicenseCategory_1 = require("./LicenseCategory");
|
|
5
|
+
const OrPolicy_1 = require("./OrPolicy");
|
|
7
6
|
const descriptions_1 = require("./descriptions");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @param licenseType the license type
|
|
11
|
-
* @returns the license category
|
|
12
|
-
*/
|
|
13
|
-
function categorizeLicense(licenseType) {
|
|
14
|
-
if (!licenseType || licenseType === 'unknown') {
|
|
15
|
-
return LicenseCategory_1.LicenseCategory.UNKNOWN;
|
|
16
|
-
}
|
|
17
|
-
// check for strong copyleft licenses
|
|
18
|
-
if (licenses_1.STRONG_COPYLEFT_LICENSES_LOWERCASE.has(licenseType.toLowerCase())) {
|
|
19
|
-
return LicenseCategory_1.LicenseCategory.STRONG_COPYLEFT;
|
|
20
|
-
}
|
|
21
|
-
// check for weak copyleft licenses
|
|
22
|
-
if (licenses_1.WEAK_COPYLEFT_LICENSES_LOWERCASE.has(licenseType.toLowerCase())) {
|
|
23
|
-
return LicenseCategory_1.LicenseCategory.WEAK_COPYLEFT;
|
|
24
|
-
}
|
|
25
|
-
// everything else is considered permissive
|
|
26
|
-
return LicenseCategory_1.LicenseCategory.PERMISSIVE;
|
|
27
|
-
}
|
|
7
|
+
const licenseClassification_1 = require("./licenseClassification");
|
|
8
|
+
const licenseExpression_1 = require("./licenseExpression");
|
|
28
9
|
/**
|
|
29
10
|
* Analyzes license data and returns comprehensive statistics.
|
|
11
|
+
*
|
|
12
|
+
* Each package is classified by its License Expression and grouped by the rendered expression,
|
|
13
|
+
* so a Dual License such as `MIT OR Apache-2.0` is counted under one key.
|
|
14
|
+
*
|
|
30
15
|
* @param report the licenses report data
|
|
16
|
+
* @param orPolicy the OR Policy used to classify Dual Licenses; defaults to {@link DEFAULT_OR_POLICY}
|
|
31
17
|
* @returns the license analysis result
|
|
32
18
|
*/
|
|
33
|
-
function analyzeLicenses(report) {
|
|
19
|
+
function analyzeLicenses(report, orPolicy = OrPolicy_1.DEFAULT_OR_POLICY) {
|
|
34
20
|
const byCategory = {
|
|
35
21
|
[LicenseCategory_1.LicenseCategory.STRONG_COPYLEFT]: 0,
|
|
36
22
|
[LicenseCategory_1.LicenseCategory.WEAK_COPYLEFT]: 0,
|
|
@@ -38,17 +24,16 @@ function analyzeLicenses(report) {
|
|
|
38
24
|
[LicenseCategory_1.LicenseCategory.UNKNOWN]: 0,
|
|
39
25
|
};
|
|
40
26
|
const byLicense = {};
|
|
27
|
+
const categoryByLicense = {};
|
|
41
28
|
const categorizedLicenses = {};
|
|
42
29
|
Object.entries(report).forEach(([packageKey, license]) => {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
30
|
+
const category = (0, licenseClassification_1.classifyLicenseExpression)(license.license, orPolicy);
|
|
31
|
+
const renderedLicense = (0, licenseExpression_1.renderLicenseExpression)(license.license);
|
|
45
32
|
// stats by category
|
|
46
33
|
byCategory[category]++;
|
|
47
34
|
// stats by specific license
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
byLicense[key] = (byLicense[key] ?? 0) + 1;
|
|
51
|
-
}
|
|
35
|
+
byLicense[renderedLicense] = (byLicense[renderedLicense] ?? 0) + 1;
|
|
36
|
+
categoryByLicense[renderedLicense] = category;
|
|
52
37
|
// memoization for lookup
|
|
53
38
|
categorizedLicenses[packageKey] = category;
|
|
54
39
|
});
|
|
@@ -58,6 +43,7 @@ function analyzeLicenses(report) {
|
|
|
58
43
|
total,
|
|
59
44
|
byCategory,
|
|
60
45
|
byLicense,
|
|
46
|
+
categoryByLicense,
|
|
61
47
|
description,
|
|
62
48
|
categoriesPresence,
|
|
63
49
|
categorizedLicenses,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LicenseExpression } from '../types';
|
|
2
|
+
import { LicenseCategory } from './LicenseCategory';
|
|
3
|
+
import type { OrPolicy } from './OrPolicy';
|
|
4
|
+
/**
|
|
5
|
+
* Classifies a License Expression into a License Category.
|
|
6
|
+
*
|
|
7
|
+
* Categories are ordered permissive < weak copyleft < strong copyleft < unknown:
|
|
8
|
+
* - a license takes the category of its License Identifier;
|
|
9
|
+
* - an Unknown License is unknown;
|
|
10
|
+
* - AND always takes its most restrictive operand;
|
|
11
|
+
* - OR takes the operand chosen by the OR Policy.
|
|
12
|
+
*
|
|
13
|
+
* @param expression the License Expression to classify
|
|
14
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
15
|
+
* @returns the License Category of the whole expression
|
|
16
|
+
* @example
|
|
17
|
+
* classifyLicenseExpression(parseLicenseExpression('MIT OR GPL-3.0-only'), 'least-restrictive');
|
|
18
|
+
* // LicenseCategory.PERMISSIVE
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyLicenseExpression(expression: LicenseExpression, orPolicy?: OrPolicy): LicenseCategory;
|
|
21
|
+
/** The copyleft that cannot be avoided for a License Expression, see {@link classifyUnavoidableCopyleft} */
|
|
22
|
+
export interface UnavoidableCopyleft {
|
|
23
|
+
/** The known copyleft that must be complied with, never unknown; permissive when no known operand requires copyleft */
|
|
24
|
+
category: LicenseCategory;
|
|
25
|
+
/** `true` when an Unknown operand could require more than `category`, so `category` is only a lower bound */
|
|
26
|
+
unidentified: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decides which copyleft cannot be avoided for a License Expression under an OR Policy.
|
|
30
|
+
*
|
|
31
|
+
* Unknown operands are ignored, so a custom license next to a copyleft one cannot hide the copyleft:
|
|
32
|
+
* - AND requires the most restrictive known operand;
|
|
33
|
+
* - OR with an Unknown operand requires what the other operand requires;
|
|
34
|
+
* - OR with two known operands follows the OR Policy;
|
|
35
|
+
* - an expression with no known operand requires no copyleft.
|
|
36
|
+
*
|
|
37
|
+
* Ignoring an Unknown operand can also understate the result (`MIT AND LicenseRef-Custom`, or an expression
|
|
38
|
+
* that is wholly Unknown), so the result says when that happened: `unidentified` is `true` when an Unknown operand
|
|
39
|
+
* could require more than the category under the OR Policy and the category is not already strong copyleft,
|
|
40
|
+
* the most restrictive known category. `MIT OR LicenseRef-Custom` is only unidentified under the most restrictive OR Policy.
|
|
41
|
+
*
|
|
42
|
+
* @param expression the License Expression to check
|
|
43
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
44
|
+
* @returns the known copyleft category, and whether an Unknown operand leaves it unidentified
|
|
45
|
+
* @example
|
|
46
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('GPL-3.0-only AND LicenseRef-Custom'));
|
|
47
|
+
* // { category: LicenseCategory.STRONG_COPYLEFT, unidentified: false }
|
|
48
|
+
* @example
|
|
49
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('MIT AND LicenseRef-Custom'));
|
|
50
|
+
* // { category: LicenseCategory.PERMISSIVE, unidentified: true }
|
|
51
|
+
*/
|
|
52
|
+
export declare function classifyUnavoidableCopyleft(expression: LicenseExpression, orPolicy?: OrPolicy): UnavoidableCopyleft;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyLicenseExpression = classifyLicenseExpression;
|
|
4
|
+
exports.classifyUnavoidableCopyleft = classifyUnavoidableCopyleft;
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const LicenseCategory_1 = require("./LicenseCategory");
|
|
7
|
+
const OrPolicy_1 = require("./OrPolicy");
|
|
8
|
+
const CATEGORY_RANK = {
|
|
9
|
+
[LicenseCategory_1.LicenseCategory.PERMISSIVE]: 0,
|
|
10
|
+
[LicenseCategory_1.LicenseCategory.WEAK_COPYLEFT]: 1,
|
|
11
|
+
[LicenseCategory_1.LicenseCategory.STRONG_COPYLEFT]: 2,
|
|
12
|
+
[LicenseCategory_1.LicenseCategory.UNKNOWN]: 3,
|
|
13
|
+
};
|
|
14
|
+
const OR_COMPARISON_STRATEGY = {
|
|
15
|
+
'most-restrictive': takeMoreRestrictive,
|
|
16
|
+
'least-restrictive': takeLessRestrictive,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Categorizes a canonical License Identifier from a parsed License Expression.
|
|
20
|
+
* Not for raw strings: an identifier outside the copyleft tables is assumed permissive,
|
|
21
|
+
* which is only safe because the parser rejects unrecognised identifiers.
|
|
22
|
+
*/
|
|
23
|
+
function categorizeLicenseId(licenseId) {
|
|
24
|
+
if (constants_1.STRONG_COPYLEFT_LICENSES_LOWERCASE.has(licenseId.toLowerCase())) {
|
|
25
|
+
return LicenseCategory_1.LicenseCategory.STRONG_COPYLEFT;
|
|
26
|
+
}
|
|
27
|
+
if (constants_1.WEAK_COPYLEFT_LICENSES_LOWERCASE.has(licenseId.toLowerCase())) {
|
|
28
|
+
return LicenseCategory_1.LicenseCategory.WEAK_COPYLEFT;
|
|
29
|
+
}
|
|
30
|
+
return LicenseCategory_1.LicenseCategory.PERMISSIVE;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Classifies a License Expression into a License Category.
|
|
34
|
+
*
|
|
35
|
+
* Categories are ordered permissive < weak copyleft < strong copyleft < unknown:
|
|
36
|
+
* - a license takes the category of its License Identifier;
|
|
37
|
+
* - an Unknown License is unknown;
|
|
38
|
+
* - AND always takes its most restrictive operand;
|
|
39
|
+
* - OR takes the operand chosen by the OR Policy.
|
|
40
|
+
*
|
|
41
|
+
* @param expression the License Expression to classify
|
|
42
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
43
|
+
* @returns the License Category of the whole expression
|
|
44
|
+
* @example
|
|
45
|
+
* classifyLicenseExpression(parseLicenseExpression('MIT OR GPL-3.0-only'), 'least-restrictive');
|
|
46
|
+
* // LicenseCategory.PERMISSIVE
|
|
47
|
+
*/
|
|
48
|
+
function classifyLicenseExpression(expression, orPolicy = OrPolicy_1.DEFAULT_OR_POLICY) {
|
|
49
|
+
switch (expression.kind) {
|
|
50
|
+
case 'license':
|
|
51
|
+
return isLicenseRef(expression.id) ? LicenseCategory_1.LicenseCategory.UNKNOWN : categorizeLicenseId(expression.id);
|
|
52
|
+
case 'unknown':
|
|
53
|
+
return LicenseCategory_1.LicenseCategory.UNKNOWN;
|
|
54
|
+
case 'and':
|
|
55
|
+
return takeMoreRestrictive(classifyLicenseExpression(expression.left, orPolicy), classifyLicenseExpression(expression.right, orPolicy));
|
|
56
|
+
case 'or':
|
|
57
|
+
return OR_COMPARISON_STRATEGY[orPolicy](classifyLicenseExpression(expression.left, orPolicy), classifyLicenseExpression(expression.right, orPolicy));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decides which copyleft cannot be avoided for a License Expression under an OR Policy.
|
|
62
|
+
*
|
|
63
|
+
* Unknown operands are ignored, so a custom license next to a copyleft one cannot hide the copyleft:
|
|
64
|
+
* - AND requires the most restrictive known operand;
|
|
65
|
+
* - OR with an Unknown operand requires what the other operand requires;
|
|
66
|
+
* - OR with two known operands follows the OR Policy;
|
|
67
|
+
* - an expression with no known operand requires no copyleft.
|
|
68
|
+
*
|
|
69
|
+
* Ignoring an Unknown operand can also understate the result (`MIT AND LicenseRef-Custom`, or an expression
|
|
70
|
+
* that is wholly Unknown), so the result says when that happened: `unidentified` is `true` when an Unknown operand
|
|
71
|
+
* could require more than the category under the OR Policy and the category is not already strong copyleft,
|
|
72
|
+
* the most restrictive known category. `MIT OR LicenseRef-Custom` is only unidentified under the most restrictive OR Policy.
|
|
73
|
+
*
|
|
74
|
+
* @param expression the License Expression to check
|
|
75
|
+
* @param orPolicy the OR Policy applied to every OR in the expression; defaults to {@link DEFAULT_OR_POLICY}
|
|
76
|
+
* @returns the known copyleft category, and whether an Unknown operand leaves it unidentified
|
|
77
|
+
* @example
|
|
78
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('GPL-3.0-only AND LicenseRef-Custom'));
|
|
79
|
+
* // { category: LicenseCategory.STRONG_COPYLEFT, unidentified: false }
|
|
80
|
+
* @example
|
|
81
|
+
* classifyUnavoidableCopyleft(parseLicenseExpression('MIT AND LicenseRef-Custom'));
|
|
82
|
+
* // { category: LicenseCategory.PERMISSIVE, unidentified: true }
|
|
83
|
+
*/
|
|
84
|
+
function classifyUnavoidableCopyleft(expression, orPolicy = OrPolicy_1.DEFAULT_OR_POLICY) {
|
|
85
|
+
const category = findKnownCopyleft(expression, orPolicy) ?? LicenseCategory_1.LicenseCategory.PERMISSIVE;
|
|
86
|
+
return {
|
|
87
|
+
category,
|
|
88
|
+
unidentified: category !== LicenseCategory_1.LicenseCategory.STRONG_COPYLEFT && hasUnknownOperand(expression, orPolicy),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether an Unknown operand could require more than {@link findKnownCopyleft} found under the OR Policy.
|
|
93
|
+
*
|
|
94
|
+
* An OR with a known operand only has this risk under the most restrictive OR Policy: under the least restrictive
|
|
95
|
+
* one the known operand can always be chosen instead of the Unknown one.
|
|
96
|
+
*/
|
|
97
|
+
function hasUnknownOperand(expression, orPolicy) {
|
|
98
|
+
switch (expression.kind) {
|
|
99
|
+
case 'unknown':
|
|
100
|
+
return true;
|
|
101
|
+
case 'license':
|
|
102
|
+
return classifyLicenseExpression(expression, orPolicy) === LicenseCategory_1.LicenseCategory.UNKNOWN;
|
|
103
|
+
case 'and':
|
|
104
|
+
return hasUnknownOperand(expression.left, orPolicy) || hasUnknownOperand(expression.right, orPolicy);
|
|
105
|
+
case 'or':
|
|
106
|
+
return orPolicy === 'least-restrictive'
|
|
107
|
+
? hasUnknownOperand(expression.left, orPolicy) && hasUnknownOperand(expression.right, orPolicy)
|
|
108
|
+
: hasUnknownOperand(expression.left, orPolicy) || hasUnknownOperand(expression.right, orPolicy);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Like {@link classifyUnavoidableCopyleft}, but `null` when no operand has a known category */
|
|
112
|
+
function findKnownCopyleft(expression, orPolicy) {
|
|
113
|
+
if (expression.kind === 'unknown') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (expression.kind === 'license') {
|
|
117
|
+
const category = classifyLicenseExpression(expression, orPolicy);
|
|
118
|
+
return category === LicenseCategory_1.LicenseCategory.UNKNOWN ? null : category;
|
|
119
|
+
}
|
|
120
|
+
const left = findKnownCopyleft(expression.left, orPolicy);
|
|
121
|
+
const right = findKnownCopyleft(expression.right, orPolicy);
|
|
122
|
+
if (left === null || right === null) {
|
|
123
|
+
return left ?? right;
|
|
124
|
+
}
|
|
125
|
+
return expression.kind === 'and' ? takeMoreRestrictive(left, right) : OR_COMPARISON_STRATEGY[orPolicy](left, right);
|
|
126
|
+
}
|
|
127
|
+
/** A custom license reference, optionally qualified by a document: `LicenseRef-X` or `DocumentRef-Y:LicenseRef-X` */
|
|
128
|
+
function isLicenseRef(id) {
|
|
129
|
+
return /^(DocumentRef-[^:]+:)?LicenseRef-/.test(id);
|
|
130
|
+
}
|
|
131
|
+
function takeMoreRestrictive(a, b) {
|
|
132
|
+
return CATEGORY_RANK[a] >= CATEGORY_RANK[b] ? a : b;
|
|
133
|
+
}
|
|
134
|
+
function takeLessRestrictive(a, b) {
|
|
135
|
+
return CATEGORY_RANK[a] <= CATEGORY_RANK[b] ? a : b;
|
|
136
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { LicenseExpression } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Parses a Raw License into a License Expression, following the SPDX License Expression syntax.
|
|
4
|
+
*
|
|
5
|
+
* Never throws: a missing or unparseable value becomes an Unknown License that keeps the original value.
|
|
6
|
+
*
|
|
7
|
+
* @param rawLicense the license declaration as written in package.json, or `null` if absent
|
|
8
|
+
* @returns the parsed License Expression
|
|
9
|
+
* @example
|
|
10
|
+
* parseLicenseExpression('MIT OR Apache-2.0');
|
|
11
|
+
* // { kind: 'or', left: { kind: 'license', id: 'MIT', declaredId: 'MIT' }, right: { kind: 'license', id: 'Apache-2.0', declaredId: 'Apache-2.0' } }
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseLicenseExpression(rawLicense: string | null): LicenseExpression;
|
|
14
|
+
/**
|
|
15
|
+
* Collects the License Identifiers mentioned in a License Expression, regardless of how they are combined.
|
|
16
|
+
*
|
|
17
|
+
* @param expression the License Expression to flatten
|
|
18
|
+
* @returns the License Identifiers in order of first appearance, without duplicates; empty for an Unknown License
|
|
19
|
+
* @example
|
|
20
|
+
* collectLicenseIds(parseLicenseExpression('(MIT OR ISC) AND (MIT OR Apache-2.0)'));
|
|
21
|
+
* // ['MIT', 'ISC', 'Apache-2.0']
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectLicenseIds(expression: LicenseExpression): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Renders a License Expression to a string, following the SPDX License Expression syntax.
|
|
26
|
+
*
|
|
27
|
+
* Adds only the parentheses required by SPDX precedence and by the right-nesting of operator chains,
|
|
28
|
+
* so that parsing the result gives back the same tree. An Unknown License renders as its original value
|
|
29
|
+
* (or `unknown` when absent) and is not expected to parse back to the same tree.
|
|
30
|
+
*
|
|
31
|
+
* @param expression the License Expression to render
|
|
32
|
+
* @returns the rendered License Expression
|
|
33
|
+
* @example
|
|
34
|
+
* renderLicenseExpression(parseLicenseExpression('(MIT OR Apache-2.0) AND ISC'));
|
|
35
|
+
* // '(MIT OR Apache-2.0) AND ISC'
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderLicenseExpression(expression: LicenseExpression): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseLicenseExpression = parseLicenseExpression;
|
|
7
|
+
exports.collectLicenseIds = collectLicenseIds;
|
|
8
|
+
exports.renderLicenseExpression = renderLicenseExpression;
|
|
9
|
+
const index_json_1 = __importDefault(require("spdx-exceptions/index.json"));
|
|
10
|
+
const spdx_expression_parse_1 = __importDefault(require("spdx-expression-parse"));
|
|
11
|
+
const deprecated_json_1 = __importDefault(require("spdx-license-ids/deprecated.json"));
|
|
12
|
+
const index_json_2 = __importDefault(require("spdx-license-ids/index.json"));
|
|
13
|
+
const SPDX_LICENSE_IDS = new Set(index_json_2.default);
|
|
14
|
+
const DEPRECATED_SPDX_LICENSE_IDS = new Set(deprecated_json_1.default);
|
|
15
|
+
const CANONICAL_ID_BY_LOWERCASE = new Map([...index_json_2.default, ...deprecated_json_1.default, ...index_json_1.default].map((id) => [id.toLowerCase(), id]));
|
|
16
|
+
const SPDX_OPERATORS = new Set(['and', 'or', 'with']);
|
|
17
|
+
/**
|
|
18
|
+
* Unambiguous spellings that are not valid SPDX License Expressions, keyed by the lowercase, trimmed value.
|
|
19
|
+
* Ambiguous values (`BSD`, `GPL`, `LGPL`) are deliberately absent: guessing a variant or version changes the legal meaning.
|
|
20
|
+
*/
|
|
21
|
+
const LICENSE_ALIASES = new Map([
|
|
22
|
+
['apache 2.0', 'Apache-2.0'],
|
|
23
|
+
['apache-2', 'Apache-2.0'],
|
|
24
|
+
['apache license 2.0', 'Apache-2.0'],
|
|
25
|
+
['mit/x11', 'MIT'],
|
|
26
|
+
['mit license', 'MIT'],
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Parses a Raw License into a License Expression, following the SPDX License Expression syntax.
|
|
30
|
+
*
|
|
31
|
+
* Never throws: a missing or unparseable value becomes an Unknown License that keeps the original value.
|
|
32
|
+
*
|
|
33
|
+
* @param rawLicense the license declaration as written in package.json, or `null` if absent
|
|
34
|
+
* @returns the parsed License Expression
|
|
35
|
+
* @example
|
|
36
|
+
* parseLicenseExpression('MIT OR Apache-2.0');
|
|
37
|
+
* // { kind: 'or', left: { kind: 'license', id: 'MIT', declaredId: 'MIT' }, right: { kind: 'license', id: 'Apache-2.0', declaredId: 'Apache-2.0' } }
|
|
38
|
+
*/
|
|
39
|
+
function parseLicenseExpression(rawLicense) {
|
|
40
|
+
if (rawLicense === null) {
|
|
41
|
+
return { kind: 'unknown', raw: rawLicense };
|
|
42
|
+
}
|
|
43
|
+
const parsed = tryParse(rawLicense) ??
|
|
44
|
+
tryParse(LICENSE_ALIASES.get(rawLicense.trim().toLowerCase())) ??
|
|
45
|
+
tryParse(fixIdentifierCase(rawLicense));
|
|
46
|
+
return parsed ?? { kind: 'unknown', raw: rawLicense };
|
|
47
|
+
}
|
|
48
|
+
function tryParse(rawLicense) {
|
|
49
|
+
if (rawLicense === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return mapSpdxParsedValueToLicenseExpression((0, spdx_expression_parse_1.default)(rawLicense));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Rewrites every identifier and operator to its canonical spelling (SPDX Annex B.2: identifiers are case-insensitive).
|
|
61
|
+
* Tokens that match no known identifier are left as written, so the re-parse rejects them.
|
|
62
|
+
*/
|
|
63
|
+
function fixIdentifierCase(rawLicense) {
|
|
64
|
+
return rawLicense.replace(/[^\s()]+/g, (token) => {
|
|
65
|
+
if (SPDX_OPERATORS.has(token.toLowerCase())) {
|
|
66
|
+
return token.toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
const plus = token.endsWith('+') ? '+' : '';
|
|
69
|
+
const id = plus ? token.slice(0, -1) : token;
|
|
70
|
+
const canonicalId = CANONICAL_ID_BY_LOWERCASE.get(id.toLowerCase()) ??
|
|
71
|
+
(/^licenseref-/i.test(id) ? `LicenseRef-${id.slice('LicenseRef-'.length)}` : id);
|
|
72
|
+
return `${canonicalId}${plus}`;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function mapSpdxParsedValueToLicenseExpression(spdxParsedValue) {
|
|
76
|
+
if ('conjunction' in spdxParsedValue) {
|
|
77
|
+
return {
|
|
78
|
+
kind: spdxParsedValue.conjunction,
|
|
79
|
+
left: mapSpdxParsedValueToLicenseExpression(spdxParsedValue.left),
|
|
80
|
+
right: mapSpdxParsedValueToLicenseExpression(spdxParsedValue.right),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const { license: declaredId, plus, exception } = spdxParsedValue;
|
|
84
|
+
return {
|
|
85
|
+
kind: 'license',
|
|
86
|
+
id: normalizeLicenseId(declaredId, plus === true),
|
|
87
|
+
declaredId,
|
|
88
|
+
...(plus && { plus: true }),
|
|
89
|
+
...(exception && { exception }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Upgrades a declared License Identifier to its canonical form:
|
|
94
|
+
* - with `+`, to the `-or-later` identifier when one exists;
|
|
95
|
+
* - a deprecated identifier, to its `-only` identifier when one exists.
|
|
96
|
+
*/
|
|
97
|
+
function normalizeLicenseId(declaredId, plus) {
|
|
98
|
+
if (plus && SPDX_LICENSE_IDS.has(`${declaredId}-or-later`)) {
|
|
99
|
+
return `${declaredId}-or-later`;
|
|
100
|
+
}
|
|
101
|
+
if (!plus && DEPRECATED_SPDX_LICENSE_IDS.has(declaredId) && SPDX_LICENSE_IDS.has(`${declaredId}-only`)) {
|
|
102
|
+
return `${declaredId}-only`;
|
|
103
|
+
}
|
|
104
|
+
return declaredId;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Collects the License Identifiers mentioned in a License Expression, regardless of how they are combined.
|
|
108
|
+
*
|
|
109
|
+
* @param expression the License Expression to flatten
|
|
110
|
+
* @returns the License Identifiers in order of first appearance, without duplicates; empty for an Unknown License
|
|
111
|
+
* @example
|
|
112
|
+
* collectLicenseIds(parseLicenseExpression('(MIT OR ISC) AND (MIT OR Apache-2.0)'));
|
|
113
|
+
* // ['MIT', 'ISC', 'Apache-2.0']
|
|
114
|
+
*/
|
|
115
|
+
function collectLicenseIds(expression) {
|
|
116
|
+
switch (expression.kind) {
|
|
117
|
+
case 'license':
|
|
118
|
+
return [expression.id];
|
|
119
|
+
case 'and':
|
|
120
|
+
case 'or':
|
|
121
|
+
return [...new Set([...collectLicenseIds(expression.left), ...collectLicenseIds(expression.right)])];
|
|
122
|
+
case 'unknown':
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Renders a License Expression to a string, following the SPDX License Expression syntax.
|
|
128
|
+
*
|
|
129
|
+
* Adds only the parentheses required by SPDX precedence and by the right-nesting of operator chains,
|
|
130
|
+
* so that parsing the result gives back the same tree. An Unknown License renders as its original value
|
|
131
|
+
* (or `unknown` when absent) and is not expected to parse back to the same tree.
|
|
132
|
+
*
|
|
133
|
+
* @param expression the License Expression to render
|
|
134
|
+
* @returns the rendered License Expression
|
|
135
|
+
* @example
|
|
136
|
+
* renderLicenseExpression(parseLicenseExpression('(MIT OR Apache-2.0) AND ISC'));
|
|
137
|
+
* // '(MIT OR Apache-2.0) AND ISC'
|
|
138
|
+
*/
|
|
139
|
+
function renderLicenseExpression(expression) {
|
|
140
|
+
switch (expression.kind) {
|
|
141
|
+
case 'unknown':
|
|
142
|
+
return expression.raw ?? 'unknown';
|
|
143
|
+
case 'or':
|
|
144
|
+
case 'and': {
|
|
145
|
+
const left = renderOperand(expression.left, expression.kind, 'left');
|
|
146
|
+
const right = renderOperand(expression.right, expression.kind, 'right');
|
|
147
|
+
return `${left} ${expression.kind.toUpperCase()} ${right}`;
|
|
148
|
+
}
|
|
149
|
+
case 'license':
|
|
150
|
+
return renderLicenseLeaf(expression);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function renderLicenseLeaf(leaf) {
|
|
154
|
+
// `+` is already absorbed into an `-or-later` identifier
|
|
155
|
+
const plus = leaf.plus && !leaf.id.endsWith('-or-later') ? '+' : '';
|
|
156
|
+
const exception = leaf.exception ? ` WITH ${leaf.exception}` : '';
|
|
157
|
+
return `${leaf.id}${plus}${exception}`;
|
|
158
|
+
}
|
|
159
|
+
function renderOperand(operand, parentKind, operandPosition) {
|
|
160
|
+
const rendered = renderLicenseExpression(operand);
|
|
161
|
+
return shouldAddParenthesis(operand, parentKind, operandPosition) ? `(${rendered})` : rendered;
|
|
162
|
+
}
|
|
163
|
+
function shouldAddParenthesis(childExpression, parentKind, childPosition) {
|
|
164
|
+
return ((parentKind === 'and' && childExpression.kind === 'or') ||
|
|
165
|
+
(childPosition === 'left' && parentKind === childExpression.kind));
|
|
166
|
+
}
|