Skip to content

Commit a4505cf

Browse files
feat(packages): add packages config to very_good.yaml (#1663)
* feat(packages): add packages config to very_good.yaml Closes #1656 Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com> * chore: simplify * chore: add missing docs and tests * fix(ci): address failing checks on PR #1663 (attempt 1/3) --------- Co-authored-by: unicoderbot[bot] <269805761+unicoderbot[bot]@users.noreply.github.com> Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com> Co-authored-by: Marcos Sevilla <me@marcossevilla.dev> Co-authored-by: Marcos Sevilla <31174242+marcossevilla@users.noreply.github.com>
1 parent 2d292dd commit a4505cf

11 files changed

Lines changed: 1086 additions & 73 deletions

File tree

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

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:pana/src/license_detection/license_detector.dart' as detector;
2121
import 'package:path/path.dart' as path;
2222
import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart';
2323
import 'package:very_good_cli/src/pubspec_lock/pubspec_lock.dart';
24+
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
2425

2526
/// Overrides the [package_config.findPackageConfig] function for testing.
2627
@visibleForTesting
@@ -65,6 +66,87 @@ typedef _DependencyLicenseMap = Map<String, Set<String>?>;
6566
/// as values.
6667
typedef _BannedDependencyLicenseMap = Map<String, Set<String>>;
6768

69+
/// Options for configuring the `very_good packages check licenses` command.
70+
class PackagesCheckLicensesOptions {
71+
PackagesCheckLicensesOptions._({
72+
required this.ignoreRetrievalFailures,
73+
required this.dependencyTypes,
74+
required this.allowedLicenses,
75+
required this.forbiddenLicenses,
76+
required this.skippedPackages,
77+
required this.reporterOutputFormat,
78+
});
79+
80+
/// Parses [ArgResults] into a [PackagesCheckLicensesOptions] instance.
81+
///
82+
/// When [config] is provided, its values are used as defaults for any
83+
/// option that was not explicitly parsed on the command line.
84+
factory PackagesCheckLicensesOptions.parse(
85+
ArgResults argResults, {
86+
VeryGoodConfig config = VeryGoodConfig.empty,
87+
}) {
88+
final licensesConfig = config.packages.check.licenses;
89+
90+
final ignoreRetrievalFailures = resolveArg(
91+
argResults,
92+
'ignore-retrieval-failures',
93+
licensesConfig.ignoreRetrievalFailures,
94+
);
95+
final dependencyTypes = resolveArg<List<String>>(
96+
argResults,
97+
'dependency-type',
98+
licensesConfig.dependencyType,
99+
);
100+
final allowedLicenses = resolveArg<List<String>>(
101+
argResults,
102+
'allowed',
103+
licensesConfig.allowed,
104+
);
105+
final forbiddenLicenses = resolveArg<List<String>>(
106+
argResults,
107+
'forbidden',
108+
licensesConfig.forbidden,
109+
);
110+
final skippedPackages = resolveArg<List<String>>(
111+
argResults,
112+
'skip-packages',
113+
licensesConfig.skipPackages,
114+
);
115+
final reporter = resolveArg<String?>(
116+
argResults,
117+
'reporter',
118+
licensesConfig.reporter,
119+
);
120+
121+
return PackagesCheckLicensesOptions._(
122+
ignoreRetrievalFailures: ignoreRetrievalFailures,
123+
dependencyTypes: dependencyTypes.toSet(),
124+
allowedLicenses: allowedLicenses.withoutBlanks,
125+
forbiddenLicenses: forbiddenLicenses.withoutBlanks,
126+
skippedPackages: skippedPackages.toSet(),
127+
reporterOutputFormat: ReporterOutputFormat.fromString(reporter),
128+
);
129+
}
130+
131+
/// Whether to disregard licenses that failed to be retrieved.
132+
final bool ignoreRetrievalFailures;
133+
134+
/// The type of dependencies to check licenses for.
135+
final Set<String> dependencyTypes;
136+
137+
/// The only licenses allowed to be used.
138+
final List<String> allowedLicenses;
139+
140+
/// The licenses denied from being used.
141+
final List<String> forbiddenLicenses;
142+
143+
/// Packages skipped from having their licenses checked.
144+
final Set<String> skippedPackages;
145+
146+
/// The format used to list all licenses.
147+
final ReporterOutputFormat? reporterOutputFormat;
148+
}
149+
68150
/// {@template packages_check_licenses_command}
69151
/// `very_good packages check licenses` command for checking packages licenses.
70152
/// {@endtemplate}
@@ -132,19 +214,27 @@ class PackagesCheckLicensesCommand extends Command<int> {
132214
usageException('Too many arguments');
133215
}
134216

135-
final ignoreFailures = _argResults['ignore-retrieval-failures'] as bool;
136-
final dependencyTypes = _argResults['dependency-type'] as List<String>;
137-
final allowedLicenses = _argResults['allowed'] as List<String>;
138-
final forbiddenLicenses = _argResults['forbidden'] as List<String>;
139-
final skippedPackages = _argResults['skip-packages'] as List<String>;
140-
final reporterOutput = _argResults['reporter'] as String?;
217+
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
218+
final targetPath = path.normalize(Directory(target).absolute.path);
141219

142-
final reporterOutputFormat = ReporterOutputFormat.fromString(
143-
reporterOutput,
220+
final VeryGoodConfig config;
221+
try {
222+
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
223+
} on VeryGoodConfigParseException catch (e) {
224+
_logger.err(
225+
'Could not read `$veryGoodConfigFileName`.\n'
226+
'${e.message}',
227+
);
228+
return ExitCode.config.code;
229+
}
230+
231+
final options = PackagesCheckLicensesOptions.parse(
232+
_argResults,
233+
config: config,
144234
);
145235

146-
allowedLicenses.removeWhere((license) => license.trim().isEmpty);
147-
forbiddenLicenses.removeWhere((license) => license.trim().isEmpty);
236+
final allowedLicenses = options.allowedLicenses;
237+
final forbiddenLicenses = options.forbiddenLicenses;
148238

149239
if (allowedLicenses.isNotEmpty && forbiddenLicenses.isNotEmpty) {
150240
usageException(
@@ -166,8 +256,6 @@ class PackagesCheckLicensesCommand extends Command<int> {
166256
);
167257
}
168258

169-
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
170-
final targetPath = path.normalize(Directory(target).absolute.path);
171259
final targetDirectory = Directory(targetPath);
172260
if (!targetDirectory.existsSync()) {
173261
_logger.err(
@@ -195,24 +283,24 @@ class PackagesCheckLicensesCommand extends Command<int> {
195283
final filteredDependencies = pubspecLock.packages.where((dependency) {
196284
if (!dependency.isPubHosted) return false;
197285

198-
if (skippedPackages.contains(dependency.name)) return false;
286+
if (options.skippedPackages.contains(dependency.name)) return false;
199287

200288
final dependencyType = dependency.type;
201-
return (dependencyTypes.contains('direct-main') &&
289+
return (options.dependencyTypes.contains('direct-main') &&
202290
dependencyType == PubspecLockPackageDependencyType.directMain) ||
203-
(dependencyTypes.contains('direct-dev') &&
291+
(options.dependencyTypes.contains('direct-dev') &&
204292
dependencyType == PubspecLockPackageDependencyType.directDev) ||
205-
(dependencyTypes.contains('transitive') &&
293+
(options.dependencyTypes.contains('transitive') &&
206294
dependencyType == PubspecLockPackageDependencyType.transitive) ||
207-
(dependencyTypes.contains('direct-overridden') &&
295+
(options.dependencyTypes.contains('direct-overridden') &&
208296
dependencyType ==
209297
PubspecLockPackageDependencyType.directOverridden);
210298
});
211299

212300
if (filteredDependencies.isEmpty) {
213301
progress.cancel();
214302
_logger.info(
215-
'''No hosted dependencies found in $targetPath of type: ${dependencyTypes.stringify()}.''',
303+
'''No hosted dependencies found in $targetPath of type: ${options.dependencyTypes.stringify()}.''',
216304
);
217305
return ExitCode.success.code;
218306
}
@@ -240,7 +328,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
240328
if (cachePackageEntry == null) {
241329
final errorMessage =
242330
'''[$dependencyName] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.''';
243-
if (!ignoreFailures) {
331+
if (!options.ignoreRetrievalFailures) {
244332
progress.cancel();
245333
_logger.err(errorMessage);
246334
return ExitCode.noInput.code;
@@ -256,7 +344,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
256344
if (!packageDirectory.existsSync()) {
257345
final errorMessage =
258346
'''[$dependencyName] Could not find package directory at $packagePath.''';
259-
if (!ignoreFailures) {
347+
if (!options.ignoreRetrievalFailures) {
260348
progress.cancel();
261349
_logger.err(errorMessage);
262350
return ExitCode.noInput.code;
@@ -284,7 +372,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
284372
} on Exception catch (e) {
285373
final errorMessage =
286374
'''[$dependencyName] Failed to detect license from $packagePath: $e''';
287-
if (!ignoreFailures) {
375+
if (!options.ignoreRetrievalFailures) {
288376
progress.cancel();
289377
_logger.err(errorMessage);
290378
return ExitCode.software.code;
@@ -326,7 +414,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
326414
_composeReport(
327415
licenses: licenses,
328416
bannedDependencies: bannedDependencies,
329-
reporterOutputFormat: reporterOutputFormat,
417+
reporterOutputFormat: options.reporterOutputFormat,
330418
),
331419
);
332420

@@ -522,15 +610,20 @@ String _composeBannedReport(_BannedDependencyLicenseMap bannedDependencies) {
522610
return '''${bannedDependencies.length} $prefix $suffix: ${bannedDependenciesList.stringify()}.''';
523611
}
524612

525-
extension on List<Object> {
613+
extension on Iterable<Object> {
526614
String stringify() {
527615
if (isEmpty) return '';
528616
if (length == 1) return first.toString();
529-
final last = removeLast();
530-
return '${join(', ')} and $last';
617+
return '${take(length - 1).join(', ')} and $last';
531618
}
532619
}
533620

621+
extension on List<String> {
622+
/// The licenses that are not blank.
623+
List<String> get withoutBlanks =>
624+
where((license) => license.trim().isNotEmpty).toList();
625+
}
626+
534627
/// Format type for listing all licenses via --reporter option.
535628
enum ReporterOutputFormat {
536629
/// List all licenses separated by a dash.

lib/src/commands/packages/commands/get.dart

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@ import 'package:meta/meta.dart';
55
import 'package:path/path.dart' as path;
66
import 'package:universal_io/io.dart';
77
import 'package:very_good_cli/src/cli/cli.dart';
8+
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
9+
10+
/// Options for configuring the `very_good packages get` command.
11+
class PackagesGetOptions {
12+
PackagesGetOptions._({required this.recursive, required this.ignore});
13+
14+
/// Parses [ArgResults] into a [PackagesGetOptions] instance.
15+
///
16+
/// When [config] is provided, its values are used as defaults for any
17+
/// option that was not explicitly parsed on the command line.
18+
factory PackagesGetOptions.parse(
19+
ArgResults argResults, {
20+
VeryGoodConfig config = VeryGoodConfig.empty,
21+
}) {
22+
final getConfig = config.packages.get;
23+
24+
final recursive = resolveArg(
25+
argResults,
26+
'recursive',
27+
getConfig.recursive,
28+
);
29+
final ignore = resolveArg<List<String>>(
30+
argResults,
31+
'ignore',
32+
getConfig.ignore,
33+
);
34+
35+
return PackagesGetOptions._(
36+
recursive: recursive,
37+
ignore: ignore.toSet(),
38+
);
39+
}
40+
41+
/// Whether to install dependencies recursively for all nested packages.
42+
final bool recursive;
43+
44+
/// Packages to exclude from installing dependencies.
45+
final Set<String> ignore;
46+
}
847

948
/// {@template packages_get_command}
1049
/// `very_good packages get` command for installing packages.
@@ -45,10 +84,22 @@ class PackagesGetCommand extends Command<int> {
4584
usageException('Too many arguments');
4685
}
4786

48-
final recursive = _argResults['recursive'] as bool;
49-
final ignore = (_argResults['ignore'] as List<String>).toSet();
5087
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
5188
final targetPath = path.normalize(Directory(target).absolute.path);
89+
90+
final VeryGoodConfig config;
91+
try {
92+
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
93+
} on VeryGoodConfigParseException catch (e) {
94+
_logger.err(
95+
'Could not read `$veryGoodConfigFileName`.\n'
96+
'${e.message}',
97+
);
98+
return ExitCode.config.code;
99+
}
100+
101+
final options = PackagesGetOptions.parse(_argResults, config: config);
102+
52103
final isFlutterInstalled = await Flutter.installed(logger: _logger);
53104
if (!isFlutterInstalled) {
54105
_logger.err(
@@ -61,8 +112,8 @@ class PackagesGetCommand extends Command<int> {
61112
try {
62113
await Flutter.pubGet(
63114
cwd: targetPath,
64-
recursive: recursive,
65-
ignore: ignore,
115+
recursive: options.recursive,
116+
ignore: options.ignore,
66117
logger: _logger,
67118
);
68119
} on PubspecNotFound catch (_) {

0 commit comments

Comments
 (0)