Skip to content

Commit bd81172

Browse files
authored
feat: add --forbidden to check licenses (#850)
1 parent 6c28edc commit bd81172

File tree

2 files changed

+214
-21
lines changed

2 files changed

+214
-21
lines changed

lib/src/commands/packages/commands/check/commands/licenses.dart

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class PackagesCheckLicensesCommand extends Command<int> {
6161
..addMultiOption(
6262
'allowed',
6363
help: 'Whitelist of allowed licenses.',
64+
)
65+
..addMultiOption(
66+
'forbidden',
67+
help: 'Blacklist of not allowed licenses.',
6468
);
6569
}
6670

@@ -89,11 +93,21 @@ class PackagesCheckLicensesCommand extends Command<int> {
8993
final ignoreFailures = _argResults['ignore-failures'] as bool;
9094
final dependencyTypes = _argResults['dependency-type'] as List<String>;
9195
final allowedLicenses = _argResults['allowed'] as List<String>;
96+
final forbiddenLicenses = _argResults['forbidden'] as List<String>;
97+
98+
if (allowedLicenses.isNotEmpty && forbiddenLicenses.isNotEmpty) {
99+
usageException(
100+
'''Cannot specify both ${styleItalic.wrap('allowed')} and ${styleItalic.wrap('forbidden')} options.''',
101+
);
102+
}
92103

93-
final invalidLicenses = _invalidLicenses(allowedLicenses);
104+
final invalidLicenses = _invalidLicenses([
105+
...allowedLicenses,
106+
...forbiddenLicenses,
107+
]);
94108
if (invalidLicenses.isNotEmpty) {
95109
_logger.warn(
96-
'''Some ${styleItalic.wrap('allowed')} licenses failed to be recognized: ${invalidLicenses.stringify()}. Refer to the documentation for a list of valid licenses.''',
110+
'''Some licenses failed to be recognized: ${invalidLicenses.stringify()}. Refer to the documentation for a list of valid licenses.''',
97111
);
98112
}
99113

@@ -172,9 +186,20 @@ class PackagesCheckLicensesCommand extends Command<int> {
172186
}
173187
}
174188

175-
final bannedDependencies = allowedLicenses.isNotEmpty
176-
? _bannedDependencies(licenses, allowedLicenses.contains)
177-
: null;
189+
late final _BannedDependencyLicenseMap? bannedDependencies;
190+
if (allowedLicenses.isNotEmpty) {
191+
bannedDependencies = _bannedDependencies(
192+
licenses: licenses,
193+
isAllowed: allowedLicenses.contains,
194+
);
195+
} else if (forbiddenLicenses.isNotEmpty) {
196+
bannedDependencies = _bannedDependencies(
197+
licenses: licenses,
198+
isAllowed: (license) => !forbiddenLicenses.contains(license),
199+
);
200+
} else {
201+
bannedDependencies = null;
202+
}
178203

179204
progress.complete(
180205
_composeReport(
@@ -227,10 +252,10 @@ List<String> _invalidLicenses(List<String> licenses) {
227252
///
228253
/// The [Map] is lazily initialized, if no dependencies are banned `null` is
229254
/// returned.
230-
_BannedDependencyLicenseMap? _bannedDependencies(
231-
_DependencyLicenseMap licenses,
232-
bool Function(String license) isAllowed,
233-
) {
255+
_BannedDependencyLicenseMap? _bannedDependencies({
256+
required _DependencyLicenseMap licenses,
257+
required bool Function(String license) isAllowed,
258+
}) {
234259
_BannedDependencyLicenseMap? bannedDependencies;
235260
for (final dependency in licenses.entries) {
236261
final name = dependency.key;

test/src/commands/packages/commands/check/commands/licenses_test.dart

Lines changed: 180 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const _expectedPackagesCheckLicensesUsage = [
2525
''' [direct-main] (default) Check for direct main dependencies.\n'''
2626
''' [transitive] Check for transitive dependencies.\n'''
2727
'\n'
28-
' --allowed Whitelist of allowed licenses.\n'
28+
''' --allowed Whitelist of allowed licenses.\n'''
29+
''' --forbidden Blacklist of not allowed licenses.\n'''
2930
'\n'
3031
'Run "very_good help" to see global options.'
3132
];
@@ -67,16 +68,29 @@ void main() {
6768
expect(command.hidden, isTrue);
6869
});
6970

70-
test(
71-
'''throws usage exception when too many rest arguments are provided''',
72-
withRunner(
73-
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
74-
final result = await commandRunner.run(
75-
[...commandArguments, 'arg1', 'arg2'],
76-
);
77-
expect(result, equals(ExitCode.usage.code));
78-
}),
79-
);
71+
group('throws usage exception', () {
72+
test(
73+
'''when too many rest arguments are provided''',
74+
withRunner(
75+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
76+
final result = await commandRunner.run(
77+
[...commandArguments, 'arg1', 'arg2'],
78+
);
79+
expect(result, equals(ExitCode.usage.code));
80+
}),
81+
);
82+
83+
test(
84+
'''when allowed and forbidden are used simultaneously''',
85+
withRunner(
86+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
87+
final result = await commandRunner.run(
88+
[...commandArguments, '--allowed', 'MIT', '--forbidden', 'BSD'],
89+
);
90+
expect(result, equals(ExitCode.usage.code));
91+
}),
92+
);
93+
});
8094

8195
group(
8296
'reports licenses',
@@ -603,7 +617,7 @@ void main() {
603617
);
604618

605619
const warningMessage =
606-
'''Some allowed licenses failed to be recognized: $invalidLicense. Refer to the documentation for a list of valid licenses.''';
620+
'''Some licenses failed to be recognized: $invalidLicense. Refer to the documentation for a list of valid licenses.''';
607621
verify(
608622
() => logger.warn(warningMessage),
609623
).called(1);
@@ -729,6 +743,160 @@ void main() {
729743
});
730744
});
731745

746+
group('forbidden', () {
747+
const forbiddenArgument = '--forbidden';
748+
749+
test(
750+
'warns when a license is not recognized',
751+
withRunner(
752+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
753+
final tempDirectory = Directory.systemTemp.createTempSync();
754+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
755+
756+
File(path.join(tempDirectory.path, pubspecLockBasename))
757+
.writeAsStringSync(_validPubspecLockContent);
758+
759+
when(() => logger.progress(any())).thenReturn(progress);
760+
761+
const invalidLicense = 'not_a_valid_license';
762+
await commandRunner.run(
763+
[
764+
...commandArguments,
765+
forbiddenArgument,
766+
invalidLicense,
767+
tempDirectory.path,
768+
],
769+
);
770+
771+
const warningMessage =
772+
'''Some licenses failed to be recognized: $invalidLicense. Refer to the documentation for a list of valid licenses.''';
773+
verify(
774+
() => logger.warn(warningMessage),
775+
).called(1);
776+
}),
777+
);
778+
779+
test(
780+
'exits when a license is forbidden',
781+
withRunner(
782+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
783+
final tempDirectory = Directory.systemTemp.createTempSync();
784+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
785+
786+
File(path.join(tempDirectory.path, pubspecLockBasename))
787+
.writeAsStringSync(_validPubspecLockContent);
788+
789+
when(() => logger.progress(any())).thenReturn(progress);
790+
791+
when(() => pubLicense.getLicense(any()))
792+
.thenAnswer((_) => Future.value({'BSD'}));
793+
794+
final result = await commandRunner.run(
795+
[
796+
...commandArguments,
797+
forbiddenArgument,
798+
'BSD',
799+
tempDirectory.path,
800+
],
801+
);
802+
803+
expect(result, ExitCode.config.code);
804+
}),
805+
);
806+
807+
group('report', () {
808+
test(
809+
'when a single license is forbidden',
810+
withRunner(
811+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
812+
final tempDirectory = Directory.systemTemp.createTempSync();
813+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
814+
815+
File(path.join(tempDirectory.path, pubspecLockBasename))
816+
.writeAsStringSync(_validMultiplePubspecLockContent);
817+
818+
when(() => logger.progress(any())).thenReturn(progress);
819+
820+
const dependency1Name = 'very_good_test_runner';
821+
when(() => pubLicense.getLicense(dependency1Name))
822+
.thenAnswer((_) => Future.value({'MIT'}));
823+
final license1LinkedMessage = link(
824+
uri: pubLicenseUri(dependency1Name),
825+
message: 'MIT',
826+
);
827+
828+
const dependency2Name = 'cli_completion';
829+
when(() => pubLicense.getLicense(dependency2Name))
830+
.thenAnswer((_) => Future.value({'BSD'}));
831+
832+
await commandRunner.run(
833+
[
834+
...commandArguments,
835+
forbiddenArgument,
836+
'MIT',
837+
tempDirectory.path,
838+
],
839+
);
840+
841+
final errorMessage =
842+
'''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).''';
843+
844+
verify(
845+
() => logger.err(errorMessage),
846+
).called(1);
847+
}),
848+
);
849+
850+
test(
851+
'when multiple licenses are forbidden',
852+
withRunner(
853+
(commandRunner, logger, pubUpdater, pubLicense, printLogs) async {
854+
final tempDirectory = Directory.systemTemp.createTempSync();
855+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
856+
857+
File(path.join(tempDirectory.path, pubspecLockBasename))
858+
.writeAsStringSync(_validMultiplePubspecLockContent);
859+
860+
when(() => logger.progress(any())).thenReturn(progress);
861+
862+
const dependency1Name = 'very_good_test_runner';
863+
when(() => pubLicense.getLicense(dependency1Name))
864+
.thenAnswer((_) => Future.value({'MIT'}));
865+
final license1LinkedMessage = link(
866+
uri: pubLicenseUri(dependency1Name),
867+
message: 'MIT',
868+
);
869+
870+
const dependency2Name = 'cli_completion';
871+
when(() => pubLicense.getLicense(dependency2Name))
872+
.thenAnswer((_) => Future.value({'BSD'}));
873+
final license2LinkedMessage = link(
874+
uri: pubLicenseUri(dependency2Name),
875+
message: 'BSD',
876+
);
877+
878+
await commandRunner.run(
879+
[
880+
...commandArguments,
881+
forbiddenArgument,
882+
'BSD',
883+
forbiddenArgument,
884+
'MIT',
885+
tempDirectory.path,
886+
],
887+
);
888+
889+
final errorMessage =
890+
'''2 dependencies have banned licenses: $dependency1Name ($license1LinkedMessage) and $dependency2Name ($license2LinkedMessage).''';
891+
892+
verify(
893+
() => logger.err(errorMessage),
894+
).called(1);
895+
}),
896+
);
897+
});
898+
});
899+
732900
group('exits with error', () {
733901
test(
734902
'when it did not find a pubspec.lock file at the target path',

0 commit comments

Comments
 (0)