Skip to content

Commit

Permalink
Add support in check-license for conjunctive (AND) licenses. (WordP…
Browse files Browse the repository at this point in the history
…ress#46801)

* Add support in `check-license` for conjunctive (AND) licenses.

Conjuctive licenses were being ignored in both the `package.json` and within
various LICENSE files. In the first case, this could lead to false negatives
$(e.g., 'MIT AND BSD' being treated as non-compatible). In the second case, the
implementation was such that only one license was returned (whichever detected
license occurred later in `licenseFileStrings`). Based on the ordering of that
list, this was likely to cause a false positive, because the non-compatible
'Apache-2.0' license occurs before any of the compatible licenses.

Progress on WordPress#38461.

* Add unit tests for checkAllCompatible

* Update CHANGELOG

* Tidy up docblock.
  • Loading branch information
bdurette authored Apr 17, 2023
1 parent 0571b73 commit 7fa9700
Show file tree
Hide file tree
Showing 8 changed files with 654 additions and 32 deletions.
4 changes: 4 additions & 0 deletions packages/scripts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@

- Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235))

### Enhancements

- License check script supports conjunctive (AND) licenses. ([46801](https://github.com/WordPress/gutenberg/pull/46801))

## 24.6.0 (2022-11-16)

## 24.5.0 (2022-11-02)
Expand Down
120 changes: 88 additions & 32 deletions packages/scripts/scripts/check-licenses.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ const licenses = [
/*
* Some packages don't included a license string in their package.json file, but they
* do have a license listed elsewhere. These files are checked for matching license strings.
* Only the first matching license file with a matching license string is considered.
*
* See: licenseFileStrings.
*/
const licenseFiles = [
'LICENCE',
Expand Down Expand Up @@ -227,6 +230,7 @@ function traverseDepTree( deps ) {

function detectTypeFromLicenseFiles( path ) {
return licenseFiles.reduce( ( detectedType, licenseFile ) => {
// If another LICENSE file already had licenses in it, use those.
if ( detectedType ) {
return detectedType;
}
Expand All @@ -235,30 +239,39 @@ function detectTypeFromLicenseFiles( path ) {

if ( existsSync( licensePath ) ) {
const licenseText = readFileSync( licensePath ).toString();

// Check if the file contains any of the strings in licenseFileStrings.
return Object.keys( licenseFileStrings ).reduce(
( stringDetectedType, licenseStringType ) => {
const licenseFileString =
licenseFileStrings[ licenseStringType ];

return licenseFileString.reduce(
( currentDetectedType, fileString ) => {
if ( licenseText.includes( fileString ) ) {
return licenseStringType;
}
return currentDetectedType;
},
stringDetectedType
);
},
detectedType
);
return detectTypeFromLicenseText( licenseText );
}

return detectedType;
}, false );
}

function detectTypeFromLicenseText( licenseText ) {
// Check if the file contains any of the strings in licenseFileStrings.
return Object.keys( licenseFileStrings ).reduce(
( stringDetectedType, licenseStringType ) => {
const licenseFileString = licenseFileStrings[ licenseStringType ];

return licenseFileString.reduce(
( currentDetectedType, fileString ) => {
if ( licenseText.includes( fileString ) ) {
if ( currentDetectedType ) {
return currentDetectedType.concat(
' AND ',
licenseStringType
);
}
return licenseStringType;
}
return currentDetectedType;
},
stringDetectedType
);
},
false
);
}

function checkDepLicense( path ) {
if ( ! path ) {
return;
Expand Down Expand Up @@ -294,11 +307,17 @@ function checkDepLicense( path ) {
licenseType = undefined;
}

if ( licenseType ) {
const allowed = licenses.find( ( allowedLicense ) =>
checkLicense( allowedLicense, licenseType )
);
if ( allowed ) {
if ( licenseType !== undefined ) {
let licenseTypes = [ licenseType ];
if ( licenseType.includes( ' AND ' ) ) {
licenseTypes = licenseType
.replace( /^\(*/g, '' )
.replace( /\)*$/, '' )
.split( ' AND ' )
.map( ( e ) => e.trim() );
}

if ( checkAllCompatible( licenseTypes, licenses ) ) {
return;
}
}
Expand All @@ -313,17 +332,54 @@ function checkDepLicense( path ) {
return;
}

// Now that we have a license to check, see if any of the allowed licenses match.
const allowed = licenses.find( ( allowedLicense ) =>
checkLicense( allowedLicense, detectedLicenseType )
let detectedLicenseTypes = [ detectedLicenseType ];
if ( detectedLicenseType.includes( ' AND ' ) ) {
detectedLicenseTypes = detectedLicenseType
.replace( /^\(*/g, '' )
.replace( /\)*$/, '' )
.split( ' AND ' )
.map( ( e ) => e.trim() );
}

if ( checkAllCompatible( detectedLicenseTypes, licenses ) ) {
return;
}

process.exitCode = 1;
process.stdout.write(
`${ ERROR } Module ${ packageInfo.name } has an incompatible license '${ licenseType }'.\n`
);
}

if ( ! allowed ) {
process.exitCode = 1;
process.stdout.write(
`${ ERROR } Module ${ packageInfo.name } has an incompatible license '${ licenseType }'.\n`
/**
* Check that all of the licenses for a package are compatible.
*
* This function is invoked when the licenses are a conjunctive ("AND") list of licenses.
* In that case, the software is only compatible if all of the licenses in the list are
* compatible.
*
* @param {Array} packageLicenses The licenses that a package is licensed under.
* @param {Array} compatibleLicenses The list of compatible licenses.
*
* @return {boolean} true if all of the packageLicenses appear in compatibleLicenses.
*/
function checkAllCompatible( packageLicenses, compatibleLicenses ) {
return packageLicenses.reduce( ( compatible, packageLicense ) => {
return (
compatible &&
compatibleLicenses.reduce(
( found, allowedLicense ) =>
found || checkLicense( allowedLicense, packageLicense ),
false
)
);
}
}, true );
}

traverseDepTree( topLevelDeps );

// Required for unit testing
module.exports = {
detectTypeFromLicenseText,
checkAllCompatible,
};
89 changes: 89 additions & 0 deletions packages/scripts/scripts/test/check-licenses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );

/**
* Internal dependencies
*/
import {
detectTypeFromLicenseText,
checkAllCompatible,
} from '../check-licenses';

describe( 'detectTypeFromLicenseText', () => {
let licenseText;

it( "should return 'Apache 2.0' when the license text is the Apache 2.0 license", () => {
licenseText = fs
.readFileSync( path.resolve( __dirname, 'licenses/apache2.txt' ) )
.toString();

expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'Apache-2.0' );
} );

it( "should return 'BSD' when the license text is the BSD 3-clause license", () => {
licenseText = fs
.readFileSync(
path.resolve( __dirname, 'licenses/bsd3clause.txt' )
)
.toString();

expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'BSD' );
} );

it( "should return 'BSD-3-Clause-W3C' when the license text is the W3C variation of the BSD 3-clause license", () => {
licenseText = fs
.readFileSync( path.resolve( __dirname, 'licenses/w3cbsd.txt' ) )
.toString();

expect( detectTypeFromLicenseText( licenseText ) ).toBe(
'BSD-3-Clause-W3C'
);
} );

it( "should return 'MIT' when the license text is the MIT license", () => {
licenseText = fs
.readFileSync( path.resolve( __dirname, 'licenses/mit.txt' ) )
.toString();

expect( detectTypeFromLicenseText( licenseText ) ).toBe( 'MIT' );
} );

it( "should return 'Apache2 AND MIT' when the license text is Apache2 followed by MIT license", () => {
licenseText = fs
.readFileSync(
path.resolve( __dirname, 'licenses/apache2-mit.txt' )
)
.toString();

expect( detectTypeFromLicenseText( licenseText ) ).toBe(
'Apache-2.0 AND MIT'
);
} );
} );

describe( 'checkAllCompatible', () => {
it( "should return 'true' when single license is in the allowed list", () => {
expect( checkAllCompatible( [ 'B' ], [ 'A', 'B', 'C' ] ) ).toBe( true );
} );

it( "should return 'false' when single license is not in the allowed list", () => {
expect( checkAllCompatible( [ 'D' ], [ 'A', 'B', 'C' ] ) ).toBe(
false
);
} );

it( "should return 'true' when all licenses are in the allowed list", () => {
expect( checkAllCompatible( [ 'A', 'C' ], [ 'A', 'B', 'C' ] ) ).toBe(
true
);
} );

it( "should return 'false' when any license is not in the allowed list", () => {
expect( checkAllCompatible( [ 'A', 'D' ], [ 'A', 'B', 'C' ] ) ).toBe(
false
);
} );
} );
Loading

0 comments on commit 7fa9700

Please sign in to comment.