Skip to content

Commit 7c3a176

Browse files
marcossevillaclaudeunicoderbot[bot]
authored
feat(licenses): add workspace support (#1657)
* feat(licenses): add workspace support * fix: ci issues * fix(licenses): exclude platform-divergent glob catch * fix(licenses): ignore platform-divergent catch clause for coverage The FileSystemException catch in _expandMembers is only reachable on Unix; on Windows glob swallows the ENOTDIR internally, leaving the catch clause line uncovered. Wrap it in coverage:ignore-start/end so coverage is 100% on both platforms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address review feedback on PR #1657 Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com> * Update test/src/pubspec/pubspec_test.dart --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: unicoderbot[bot] <269805761+unicoderbot[bot]@users.noreply.github.com> Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com>
1 parent d1b694b commit 7c3a176

9 files changed

Lines changed: 1616 additions & 124 deletions

File tree

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,24 @@ import 'package:package_config/package_config.dart' as package_config;
2020
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';
23+
import 'package:very_good_cli/src/pubspec/pubspec.dart';
2324
import 'package:very_good_cli/src/pubspec_lock/pubspec_lock.dart';
25+
import 'package:very_good_cli/src/pubspec_workspace/pubspec_workspace.dart';
2426
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
2527

2628
/// Overrides the [package_config.findPackageConfig] function for testing.
2729
@visibleForTesting
2830
Future<package_config.PackageConfig?> Function(Directory directory)?
2931
findPackageConfigOverride;
3032

33+
/// Overrides the [resolveWorkspaceDependencies] function for testing.
34+
@visibleForTesting
35+
Map<String, PubspecDependencyType>? Function(
36+
Directory directory, {
37+
required Logger logger,
38+
})?
39+
resolveWorkspaceOverride;
40+
3141
/// Overrides the [detector.detectLicense] function for testing.
3242
@visibleForTesting
3343
Future<detector.Result> Function(String, double)? detectLicenseOverride;
@@ -250,7 +260,15 @@ class PackagesCheckLicensesCommand extends Command<int> {
250260
final pubspecLockFile = File(path.join(targetPath, pubspecLockBasename));
251261
if (!pubspecLockFile.existsSync()) {
252262
progress.cancel();
253-
_logger.err('Could not find a $pubspecLockBasename in $targetPath');
263+
if (declaresWorkspaceResolution(targetDirectory)) {
264+
_logger.err(
265+
'Could not find a $pubspecLockBasename in $targetPath.\n'
266+
'This package resolves as part of a Pub workspace. '
267+
'Run the command from the workspace root instead.',
268+
);
269+
} else {
270+
_logger.err('Could not find a $pubspecLockBasename in $targetPath');
271+
}
254272
return ExitCode.noInput.code;
255273
}
256274

@@ -261,21 +279,26 @@ class PackagesCheckLicensesCommand extends Command<int> {
261279
return ExitCode.noInput.code;
262280
}
263281

282+
final resolveWorkspace =
283+
resolveWorkspaceOverride ?? resolveWorkspaceDependencies;
284+
final workspaceDeps = resolveWorkspace(targetDirectory, logger: _logger);
285+
264286
final filteredDependencies = pubspecLock.packages.where((dependency) {
265287
if (!dependency.isPubHosted) return false;
266288

267289
if (options.skippedPackages.contains(dependency.name)) return false;
268290

269-
final dependencyType = dependency.type;
291+
final dependencyType = workspaceDeps == null
292+
? dependency.type
293+
: workspaceDeps[dependency.name] ?? PubspecDependencyType.transitive;
270294
return (options.dependencyTypes.contains('direct-main') &&
271-
dependencyType == PubspecLockPackageDependencyType.directMain) ||
295+
dependencyType == PubspecDependencyType.directMain) ||
272296
(options.dependencyTypes.contains('direct-dev') &&
273-
dependencyType == PubspecLockPackageDependencyType.directDev) ||
297+
dependencyType == PubspecDependencyType.directDev) ||
274298
(options.dependencyTypes.contains('transitive') &&
275-
dependencyType == PubspecLockPackageDependencyType.transitive) ||
299+
dependencyType == PubspecDependencyType.transitive) ||
276300
(options.dependencyTypes.contains('direct-overridden') &&
277-
dependencyType ==
278-
PubspecLockPackageDependencyType.directOverridden);
301+
dependencyType == PubspecDependencyType.directOverridden);
279302
});
280303

281304
if (filteredDependencies.isEmpty) {

lib/src/pubspec/pubspec.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/// Shared pubspec-domain primitives built on top of `package:pubspec_parse`.
2+
///
3+
/// The `packages check licenses` command reads dependency information from two
4+
/// different sources: a `pubspec.lock` file (via `pubspec_lock.dart`) and the
5+
/// `pubspec.yaml` files of a Pub workspace (via `pubspec_workspace.dart`). Both
6+
/// classify dependencies with the same model, so that model lives here — a
7+
/// common ancestor both import — instead of being duplicated across the two
8+
/// sibling parsers.
9+
library;
10+
11+
import 'dart:io';
12+
import 'package:pubspec_parse/pubspec_parse.dart';
13+
14+
export 'package:pubspec_parse/pubspec_parse.dart';
15+
16+
/// {@template pubspec_dependency_type}
17+
/// The classification of a package dependency.
18+
/// {@endtemplate}
19+
enum PubspecDependencyType {
20+
/// Another package that your package needs to work.
21+
///
22+
/// See also:
23+
///
24+
/// * [Dart's dependency documentation](https://dart.dev/tools/pub/dependencies)
25+
directMain._('direct main'),
26+
27+
/// Another package that your package needs during development.
28+
///
29+
/// See also:
30+
///
31+
/// * [Dart's developer dependency documentation](https://dart.dev/tools/pub/dependencies#dev-dependencies)
32+
directDev._('direct dev'),
33+
34+
/// A dependency that your package indirectly uses because one of its
35+
/// dependencies requires it.
36+
///
37+
/// See also:
38+
///
39+
/// * [Dart's transitive dependency documentation](https://dart.dev/tools/pub/glossary#transitive-)
40+
transitive._('transitive'),
41+
42+
/// A dependency that your package overrides that is not already a
43+
/// `direct main` or `direct dev` dependency.
44+
///
45+
/// See also:
46+
///
47+
/// * [Dart's dependency override documentation](https://dart.dev/tools/pub/dependencies#dependency-overrides)
48+
directOverridden._('direct overridden');
49+
50+
const PubspecDependencyType._(this.value);
51+
52+
/// Parses a [PubspecDependencyType] from its `pubspec.lock` textual form.
53+
///
54+
/// Throws an [ArgumentError] if the string is not a valid dependency type.
55+
factory PubspecDependencyType.parse(String value) {
56+
if (_valueMap.containsKey(value)) return _valueMap[value]!;
57+
58+
throw ArgumentError.value(
59+
value,
60+
'value',
61+
'Invalid PubspecDependencyType value.',
62+
);
63+
}
64+
65+
static final Map<String, PubspecDependencyType> _valueMap = {
66+
for (final type in PubspecDependencyType.values) type.value: type,
67+
};
68+
69+
/// The textual representation of the [PubspecDependencyType] as it appears in
70+
/// the `dependency` field of a `pubspec.lock` file.
71+
final String value;
72+
}
73+
74+
/// Tolerantly parses a [Pubspec] from [pubspecFile].
75+
///
76+
/// Returns `null` when the file does not exist or cannot be parsed. Parsing is
77+
/// lenient so valid-but-unmodeled keys (e.g. a `flutter:` block) do not throw.
78+
Pubspec? tryParsePubspec(File pubspecFile) {
79+
if (!pubspecFile.existsSync()) return null;
80+
try {
81+
return Pubspec.parse(pubspecFile.readAsStringSync(), lenient: true);
82+
// Tolerate any malformed pubspec by returning null instead of throwing.
83+
// ignore: avoid_catches_without_on_clauses
84+
} catch (_) {
85+
return null;
86+
}
87+
}

lib/src/pubspec_lock/pubspec_lock.dart

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ library;
99
import 'dart:collection';
1010

1111
import 'package:equatable/equatable.dart';
12+
import 'package:very_good_cli/src/pubspec/pubspec.dart';
1213
import 'package:yaml/yaml.dart';
1314

1415
/// {@template PubspecLockParseException}
@@ -88,7 +89,7 @@ class PubspecLockPackage extends Equatable {
8889
required YamlMap data,
8990
}) {
9091
final dependency = data['dependency'] as String;
91-
final dependencyType = PubspecLockPackageDependencyType.parse(dependency);
92+
final dependencyType = PubspecDependencyType.parse(dependency);
9293

9394
final source = data['source'] as String;
9495
late final bool isPubHosted;
@@ -110,73 +111,12 @@ class PubspecLockPackage extends Equatable {
110111
/// The name of the dependency.
111112
final String name;
112113

113-
/// {@macro PubspecLockDependencyType}
114-
final PubspecLockPackageDependencyType type;
114+
/// {@macro pubspec_dependency_type}
115+
final PubspecDependencyType type;
115116

116117
/// Whether the dependency is hosted on pub.dev or not.
117118
final bool isPubHosted;
118119

119120
@override
120121
List<Object?> get props => [type, isPubHosted];
121122
}
122-
123-
/// {@template PubspecLockDependencyType}
124-
/// The type of a [PubspecLockPackage].
125-
/// {@endtemplate}
126-
enum PubspecLockPackageDependencyType {
127-
/// Another package that your package needs to work.
128-
///
129-
/// See also:
130-
///
131-
/// * [Dart's dependency documentation](https://dart.dev/tools/pub/dependencies)
132-
directMain._('direct main'),
133-
134-
/// Another package that your package needs during development.
135-
///
136-
/// See also:
137-
///
138-
/// * [Dart's developer dependency documentation](https://dart.dev/tools/pub/dependencies#dev-dependencies)
139-
directDev._('direct dev'),
140-
141-
/// A dependency that your package indirectly uses because one of its
142-
/// dependencies requires it.
143-
///
144-
/// See also:
145-
///
146-
/// * [Dart's transitive dependency documentation](https://dart.dev/tools/pub/glossary#transitive-)
147-
transitive._('transitive'),
148-
149-
/// A dependency that your package overrides that is not already a
150-
/// `direct main` or `direct dev` dependency.
151-
///
152-
/// See also:
153-
///
154-
/// * [Dart's dependency override documentation](https://dart.dev/tools/pub/dependencies#dependency-overrides)
155-
directOverridden._('direct overridden');
156-
157-
const PubspecLockPackageDependencyType._(this.value);
158-
159-
/// Parses a [PubspecLockPackageDependencyType] from a string.
160-
///
161-
/// Throws an [ArgumentError] if the string is not a valid dependency type.
162-
factory PubspecLockPackageDependencyType.parse(String value) {
163-
if (_valueMap.containsKey(value)) {
164-
return _valueMap[value]!;
165-
}
166-
167-
throw ArgumentError.value(
168-
value,
169-
'value',
170-
'Invalid PubspecLockPackageDependencyType value.',
171-
);
172-
}
173-
174-
static Map<String, PubspecLockPackageDependencyType> _valueMap = {
175-
for (final type in PubspecLockPackageDependencyType.values)
176-
type.value: type,
177-
};
178-
179-
/// The string representation of the [PubspecLockPackageDependencyType]
180-
/// as it appears in a pubspec.lock file.
181-
final String value;
182-
}

0 commit comments

Comments
 (0)