Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check license against SPDX license list #701

Merged
merged 7 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ The npm-package-json-lint analysis only affects the `package.json` file in the r

To adjust the rules to be used, add an [`.npmpackagejsonlintrc.json`](https://npmpackagejsonlint.org/docs/en/rcfile-example) file to the root directory of your module or application. You may [extend](https://npmpackagejsonlint.org/docs/en/configuration#how-to-use-a-shared-config-module) the `npm-package-json-lint-config-tnw/app.json` configuration, the `npm-package-json-lint-config-tnw/lib.json` configuration or any configuration you have made yourself.

In addition to the linting provided by [npm-package-json-lint](https://npmpackagejsonlint.org/), roboter checks the license provided
by you against the [SPDX license list](https://github.com/spdx/license-list-XML). The default behavior is to reject licenses that are not present on the list, as well as deprecated licenses. If you wish to use a more exotic license that is not listed by SPDX, you may set `allowUnsupportedLicenseForThisPackage` to `true` in `licenseCheck.json`. Deprecated licenses can be enabled in a similar way by setting `allowDeprecatedLicenseForThisPackage` to `true` in `licenseCheck.json`.

## The `build` task

If you want to use TypeScript, add the required `tsconfig.json` file to the root of your package to enable compilation on build.
Expand Down
2 changes: 2 additions & 0 deletions lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class GitNotInUpdateableState extends defekt({ code: 'GitNotInUpdateableState' }
class LicenseCheckConfigurationNotFound extends defekt({ code: 'LicenseCheckConfigurationNotFound' }) {}
class LicenseCheckConfigurationMalformed extends defekt({ code: 'LicenseCheckConfigurationMalformed' }) {}
class LicenseCheckFailed extends defekt({ code: 'LicenseCheckFailed' }) {}
class LicenseDeprecated extends defekt({ code: 'LicenseDeprecated' }) {}
class LicenseIncompatible extends defekt({ code: 'LicenseIncompatible' }) {}
class LicenseNotFound extends defekt({ code: 'LicenseNotFound' }) {}
class LicenseNotSupported extends defekt({ code: 'LicenseNotSupported' }) {}
Expand Down Expand Up @@ -85,6 +86,7 @@ export {
LicenseCheckConfigurationNotFound,
LicenseCheckConfigurationMalformed,
LicenseCheckFailed,
LicenseDeprecated,
LicenseIncompatible,
LicenseNotFound,
LicenseNotSupported,
Expand Down
39 changes: 39 additions & 0 deletions lib/steps/analyze/checkLicenseExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import deprecatedSpdxLicenseIds from 'spdx-license-ids/deprecated.json';
import { getLicense } from '../license/getLicense';
import { getLicenseCheckConfiguration } from '../license/getLicenseCheckConfiguration';
import { error, Result, value } from 'defekt';
import * as errors from '../../errors';

const checkLicenseExpression = async function ({
applicationRoot
}: {
applicationRoot: string;
}): Promise<Result<undefined, errors.LicenseNotFound | errors.LicenseNotSupported | errors.LicenseDeprecated>> {
const licenseCheckConfiguration = (await getLicenseCheckConfiguration({ absoluteDirectory: applicationRoot })).unwrapOrThrow();
const licenseResult = await getLicense({ absoluteDirectory: applicationRoot, licenseCheckConfiguration });

if (licenseResult.hasError()) {
if (licenseResult.error.code === errors.LicenseNotSupported.code && licenseCheckConfiguration.allowUnsupportedLicenseForThisPackage) {
return value();
}

return error(new errors.LicenseNotSupported({
message: `The given license is not supported, please check your spelling, or disable this check by setting 'allowUnsupportedLicenseForThisPackage' in licenseCheck.json.`,
cause: licenseResult.error
}));
}

const license = licenseResult.unwrapOrThrow();

if (deprecatedSpdxLicenseIds.includes(license) && !licenseCheckConfiguration.allowDeprecatedLicenseForThisPackage) {
return error(new errors.LicenseDeprecated({
message: `${license} is deprecated, please consider using an updated version of this license, or disable this check by setting 'allowDeprecatedLicenseForThisPackage' in licenseCheck.json.`
}));
}

return value();
};

export {
checkLicenseExpression
};
2 changes: 2 additions & 0 deletions lib/steps/license/LicenseCheckConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
interface LicenseCheckConfiguration {
compatibleLicenses: string[];
knownPackageLicenses?: Record<string, Record<string, string>>;
allowUnsupportedLicenseForThisPackage?: boolean;
allowDeprecatedLicenseForThisPackage?: boolean;
}

export type {
Expand Down
6 changes: 6 additions & 0 deletions lib/steps/license/parseLicenseCheckConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ errors.LicenseCheckConfigurationMalformed
}
}
}
},
allowUnsupportedLicenseForThisPackage: {
type: 'boolean'
},
allowDeprecatedLicenseForThisPackage: {
type: 'boolean'
}
},
required: [ 'compatibleLicenses' ],
Expand Down
30 changes: 30 additions & 0 deletions lib/tasks/analyzeTask.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buntstift } from 'buntstift';
import { checkLicenseExpression } from '../steps/analyze/checkLicenseExpression';
import { lintCode } from '../steps/analyze/lintCode';
import { lintPackageJson } from '../steps/analyze/lintPackageJson';
import { error, Result, value } from 'defekt';
Expand Down Expand Up @@ -33,6 +34,35 @@ const analyzeTask = async function ({ applicationRoot }: {

stopWaiting = buntstift.wait();

const checkLicenseExpressionResult = await checkLicenseExpression({ applicationRoot });

stopWaiting();

if (checkLicenseExpressionResult.hasError()) {
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check, default-case
switch (checkLicenseExpressionResult.error.code) {
case errors.LicenseNotFound.code: {
buntstift.error('No license was found for this package.');
buntstift.raw(`${checkLicenseExpressionResult.error.message}\n`);
break;
}
case errors.LicenseNotSupported.code: {
buntstift.error('The given license is not a valid SPDX expression.');
buntstift.raw(`${checkLicenseExpressionResult.error.message}\n`);
break;
}
case errors.LicenseDeprecated.code: {
buntstift.error('The given license is deprecated.');
buntstift.raw(`${checkLicenseExpressionResult.error.message}\n`);
break;
}
}

return error(new errors.AnalysisFailed());
}

stopWaiting = buntstift.wait();

const lintCodeResult = await lintCode({ applicationRoot });

stopWaiting();
Expand Down
16 changes: 9 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@
"nodeenv": "3.0.71",
"normalize-path": "3.0.0",
"npm-package-json-lint": "5.2.3",
"npm-package-json-lint-config-tnw": "1.1.1",
"npm-package-json-lint-config-tnw": "1.1.2",
"processenv": "3.0.8",
"semver": "7.3.5",
"shelljs": "0.8.4",
"spdx-expression-parse": "3.0.1",
"spdx-license-ids": "3.0.10",
"spdx-satisfies": "5.0.1",
"ts-node": "10.2.1",
"type-fest": "2.0.0",
Expand Down
74 changes: 74 additions & 0 deletions test/integration/analyzeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,78 @@ suite('analyze', function (): void {
assert.that(stripAnsi(error.stderr)).is.containing('Malformed package.json found.');
}
);

testWithFixture(
'fails with deprecated package license.',
[ 'analyze', 'with-deprecated-license' ],
async (fixture): Promise<void> => {
const roboterResult = await runCommand('npx roboter analyze', {
cwd: fixture.absoluteTestDirectory,
silent: true
});

if (roboterResult.hasValue()) {
throw new Error(`The command should have failed, but didn't.`);
}

const { error } = roboterResult;

assert.that(error.exitCode).is.equalTo(1);
assert.that(stripAnsi(error.stdout)).is.containing(stripIndent`
GPL-2.0 is deprecated, please consider using an updated version of this license
`);
assert.that(stripAnsi(error.stderr)).is.containing('The given license is deprecated.');
}
);

testWithFixture(
'fails with unsupported package license.',
[ 'analyze', 'with-unsupported-license' ],
async (fixture): Promise<void> => {
const roboterResult = await runCommand('npx roboter analyze', {
cwd: fixture.absoluteTestDirectory,
silent: true
});

if (roboterResult.hasValue()) {
throw new Error(`The command should have failed, but didn't.`);
}

const { error } = roboterResult;

assert.that(error.exitCode).is.equalTo(1);
assert.that(stripAnsi(error.stdout)).is.containing(stripIndent`
The given license is not supported, please check your spelling
`);
assert.that(stripAnsi(error.stderr)).is.containing('The given license is not a valid SPDX expression.');
}
);

testWithFixture(
'succeeds with deprecated package license and allowDeprecatedLicenseForThisPackage set to true.',
[ 'analyze', 'with-deprecated-license-with-exception' ],
async (fixture): Promise<void> => {
const roboterResult = await runCommand('npx roboter analyze', {
cwd: fixture.absoluteTestDirectory,
silent: true
});

assert.that(roboterResult).is.aValue();
assert.that(roboterResult.unwrapOrThrow().exitCode).is.equalTo(0);
}
);

testWithFixture(
'succeeds with unsupported package license and allowUnsupportedLicenseForThisPackage set to true.',
[ 'analyze', 'with-unsupported-license-with-exception' ],
async (fixture): Promise<void> => {
const roboterResult = await runCommand('npx roboter analyze', {
cwd: fixture.absoluteTestDirectory,
silent: true
});

assert.that(roboterResult).is.aValue();
assert.that(roboterResult.unwrapOrThrow().exitCode).is.equalTo(0);
}
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compatibleLicenses": [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC-BY-3.0",
"CC-BY-4.0",
"CC0-1.0",
"MIT",
"ISC",
"Python-2.0"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

// eslint-disable-next-line no-unused-vars
const x = 42;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compatibleLicenses": [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC-BY-3.0",
"CC-BY-4.0",
"CC0-1.0",
"MIT",
"ISC",
"Python-2.0"
],
"allowDeprecatedLicenseForThisPackage": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "test-package",
"version": "0.0.1",
"description": "a test-package.",
"contributors": [],
"private": false,
"main": "",
"types": "",
"engines": {},
"dependencies": {},
"devDependencies": {},
"scripts": {},
"repository": {},
"keywords": [],
"license": "GPL-2.0"
}
4 changes: 4 additions & 0 deletions test/shared/fixtures/analyze/with-deprecated-license/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

// eslint-disable-next-line no-unused-vars
const x = 42;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compatibleLicenses": [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC-BY-3.0",
"CC-BY-4.0",
"CC0-1.0",
"MIT",
"ISC",
"Python-2.0"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "test-package",
"version": "0.0.1",
"description": "a test-package.",
"contributors": [],
"private": false,
"main": "",
"types": "",
"engines": {},
"dependencies": {},
"devDependencies": {},
"scripts": {},
"repository": {},
"keywords": [],
"license": "GPL-2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compatibleLicenses": [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC-BY-3.0",
"CC-BY-4.0",
"CC0-1.0",
"MIT",
"ISC",
"Python-2.0"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compatibleLicenses": [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC-BY-3.0",
"CC-BY-4.0",
"CC0-1.0",
"MIT",
"ISC",
"Python-2.0"
]
}
Loading