Skip to content

Commit 6c28edc

Browse files
authored
feat: add --allowed to check licenses (#848)
1 parent 5479917 commit 6c28edc

File tree

2 files changed

+310
-24
lines changed

2 files changed

+310
-24
lines changed

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

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,26 @@ import 'package:meta/meta.dart';
77
import 'package:path/path.dart' as path;
88
import 'package:pubspec_lock/pubspec_lock.dart';
99
import 'package:very_good_cli/src/pub_license/pub_license.dart';
10+
import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart';
1011

1112
/// The basename of the pubspec lock file.
1213
@visibleForTesting
1314
const pubspecLockBasename = 'pubspec.lock';
1415

16+
/// The URI for the pub.dev license page for the given [packageName].
17+
@visibleForTesting
18+
Uri pubLicenseUri(String packageName) =>
19+
Uri.parse('https://pub.dev/packages/$packageName/license');
20+
21+
/// Defines a [Map] with dependencies as keys and their licenses as values.
22+
///
23+
/// If a dependency's license failed to be retrieved its license will be `null`.
24+
typedef _DependencyLicenseMap = Map<String, Set<String>?>;
25+
26+
/// Defines a [Map] with banned dependencies as keys and their banned licenses
27+
/// as values.
28+
typedef _BannedDependencyLicenseMap = Map<String, Set<String>>;
29+
1530
/// {@template packages_check_licenses_command}
1631
/// `very_good packages check licenses` command for checking packages licenses.
1732
/// {@endtemplate}
@@ -42,6 +57,10 @@ class PackagesCheckLicensesCommand extends Command<int> {
4257
'transitive': 'Check for transitive dependencies.',
4358
},
4459
defaultsTo: ['direct-main'],
60+
)
61+
..addMultiOption(
62+
'allowed',
63+
help: 'Whitelist of allowed licenses.',
4564
);
4665
}
4766

@@ -69,6 +88,14 @@ class PackagesCheckLicensesCommand extends Command<int> {
6988

7089
final ignoreFailures = _argResults['ignore-failures'] as bool;
7190
final dependencyTypes = _argResults['dependency-type'] as List<String>;
91+
final allowedLicenses = _argResults['allowed'] as List<String>;
92+
93+
final invalidLicenses = _invalidLicenses(allowedLicenses);
94+
if (invalidLicenses.isNotEmpty) {
95+
_logger.warn(
96+
'''Some ${styleItalic.wrap('allowed')} licenses failed to be recognized: ${invalidLicenses.stringify()}. Refer to the documentation for a list of valid licenses.''',
97+
);
98+
}
7299

73100
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
74101
final targetPath = path.normalize(Directory(target).absolute.path);
@@ -114,7 +141,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
114141
final licenses = <String, Set<String>?>{};
115142
for (final dependency in filteredDependencies) {
116143
progress.update(
117-
'Collecting licenses of ${licenses.length}/${filteredDependencies.length} packages',
144+
'Collecting licenses of ${licenses.length}/${filteredDependencies.length} packages.',
118145
);
119146

120147
final dependencyName = dependency.package();
@@ -145,7 +172,21 @@ class PackagesCheckLicensesCommand extends Command<int> {
145172
}
146173
}
147174

148-
progress.complete(_composeReport(licenses));
175+
final bannedDependencies = allowedLicenses.isNotEmpty
176+
? _bannedDependencies(licenses, allowedLicenses.contains)
177+
: null;
178+
179+
progress.complete(
180+
_composeReport(
181+
licenses: licenses,
182+
bannedDependencies: bannedDependencies,
183+
),
184+
);
185+
186+
if (bannedDependencies != null) {
187+
_logger.err(_composeBannedReport(bannedDependencies));
188+
return ExitCode.config.code;
189+
}
149190

150191
return ExitCode.success.code;
151192
}
@@ -163,28 +204,119 @@ PubspecLock? _tryParsePubspecLock(File pubspecLockFile) {
163204
}
164205
}
165206

207+
/// Verifies that all [licenses] are valid license inputs.
208+
///
209+
/// Valid license inputs are:
210+
/// - [SpdxLicense] values.
211+
///
212+
/// Returns a [List] of invalid licenses, if all licenses are valid the list
213+
/// will be empty.
214+
List<String> _invalidLicenses(List<String> licenses) {
215+
final invalidLicenses = <String>[];
216+
for (final license in licenses) {
217+
final parsedLicense = SpdxLicense.tryParse(license);
218+
if (parsedLicense == null) {
219+
invalidLicenses.add(license);
220+
}
221+
}
222+
223+
return invalidLicenses;
224+
}
225+
226+
/// Returns a [Map] of banned dependencies and their banned licenses.
227+
///
228+
/// The [Map] is lazily initialized, if no dependencies are banned `null` is
229+
/// returned.
230+
_BannedDependencyLicenseMap? _bannedDependencies(
231+
_DependencyLicenseMap licenses,
232+
bool Function(String license) isAllowed,
233+
) {
234+
_BannedDependencyLicenseMap? bannedDependencies;
235+
for (final dependency in licenses.entries) {
236+
final name = dependency.key;
237+
final license = dependency.value;
238+
if (license == null) continue;
239+
240+
for (final licenseType in license) {
241+
if (isAllowed(licenseType)) continue;
242+
243+
bannedDependencies ??= <String, Set<String>>{};
244+
bannedDependencies.putIfAbsent(name, () => <String>{});
245+
bannedDependencies[name]!.add(licenseType);
246+
}
247+
}
248+
249+
return bannedDependencies;
250+
}
251+
166252
/// Composes a human friendly [String] to report the result of the retrieved
167253
/// licenses.
168-
String _composeReport(Map<String, Set<String>?> licenses) {
254+
///
255+
/// If [bannedDependencies] is provided those banned licenses will be
256+
/// highlighted in red.
257+
String _composeReport({
258+
required _DependencyLicenseMap licenses,
259+
required _BannedDependencyLicenseMap? bannedDependencies,
260+
}) {
261+
final bannedLicenseTypes =
262+
bannedDependencies?.values.fold(<String>{}, (previousValue, licenses) {
263+
if (licenses.isEmpty) return previousValue;
264+
return previousValue..addAll(licenses);
265+
});
169266
final licenseTypes =
170-
licenses.values.fold(<String>{}, (previousValue, element) {
171-
if (element == null) return previousValue;
172-
return previousValue..addAll(element);
267+
licenses.values.fold(<String>{}, (previousValue, licenses) {
268+
if (licenses == null) return previousValue;
269+
return previousValue..addAll(licenses);
270+
});
271+
final coloredLicenseTypes = licenseTypes.map((license) {
272+
if (bannedLicenseTypes != null && bannedLicenseTypes.contains(license)) {
273+
return red.wrap(license)!;
274+
}
275+
return green.wrap(license)!;
173276
});
277+
174278
final licenseCount = licenses.values.fold<int>(0, (previousValue, element) {
175279
if (element == null) return previousValue;
176280
return previousValue + element.length;
177281
});
178282

179283
final licenseWord = licenseCount == 1 ? 'license' : 'licenses';
180284
final packageWord = licenses.length == 1 ? 'package' : 'packages';
181-
final suffix = licenseTypes.isEmpty
285+
final suffix = coloredLicenseTypes.isEmpty
182286
? ''
183-
: ' of type: ${licenseTypes.toList().stringify()}';
287+
: ' of type: ${coloredLicenseTypes.toList().stringify()}';
184288

185289
return '''Retrieved $licenseCount $licenseWord from ${licenses.length} $packageWord$suffix.''';
186290
}
187291

292+
String _composeBannedReport(_BannedDependencyLicenseMap bannedDependencies) {
293+
final bannedDependenciesList = bannedDependencies.entries.fold(
294+
<String>[],
295+
(previousValue, element) {
296+
final dependencyName = element.key;
297+
final dependencyLicenses = element.value;
298+
299+
final text = '$dependencyName (${link(
300+
uri: pubLicenseUri(dependencyName),
301+
message: dependencyLicenses.toList().stringify(),
302+
)})';
303+
return previousValue..add(text);
304+
},
305+
);
306+
final bannedLicenseTypes =
307+
bannedDependencies.values.fold(<String>{}, (previousValue, licenses) {
308+
if (licenses.isEmpty) return previousValue;
309+
return previousValue..addAll(licenses);
310+
});
311+
312+
final prefix =
313+
bannedDependencies.length == 1 ? 'dependency has' : 'dependencies have';
314+
final suffix =
315+
bannedLicenseTypes.length == 1 ? 'a banned license' : 'banned licenses';
316+
317+
return '''${bannedDependencies.length} $prefix $suffix: ${bannedDependenciesList.stringify()}.''';
318+
}
319+
188320
extension on List<Object> {
189321
String stringify() {
190322
if (isEmpty) return '';

0 commit comments

Comments
 (0)