From 14c613f667918defb8fc67a655fadcfd3c611f31 Mon Sep 17 00:00:00 2001
From: Paul-Anatole CLAUDOT
Date: Tue, 4 Nov 2025 09:30:34 +0100
Subject: [PATCH 1/2] Add new config parameters to better handle workspace
(allow pinned and exclude workspace packages)
---
README.md | 30 ++++++++
example/example.md | 14 +++-
lib/src/dependency_validator.dart | 26 +++++--
lib/src/pubspec_config.dart | 8 ++
lib/src/pubspec_config.g.dart | 58 ++++++++++-----
test/workspace_test.dart | 118 ++++++++++++++++++++++++++++++
6 files changed, 229 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
index dc4a9e1..45b1fa5 100644
--- a/README.md
+++ b/README.md
@@ -67,3 +67,33 @@ and your sub-packages should have `resolution: workspace` in their `pubspec.yaml
```bash
$ dart run dependency_validator -C pkg1
```
+
+### Workspace Configuration
+
+When working with workspaces, you can configure workspace-specific settings in the root package's `dart_dependency_validator.yaml` file:
+
+```yaml
+# dart_dependency_validator.yaml (in workspace root)
+
+# Allow pinned packages across the workspace
+allow_pins: true
+
+# Ignore specific packages in all workspace sub-packages
+workspace_global_ignore:
+ - some_package
+ - another_package
+
+# Skip validation for specific workspace packages
+workspace_package_ignore:
+ - pkg1
+ - pkg2
+```
+
+#### Configuration Inheritance
+
+By default, sub-packages inherit certain configuration settings from the workspace root:
+- `workspace_global_ignore`: Packages listed here will be ignored in all sub-packages
+- `allow_pins`: If set to `true` in the workspace root, sub-packages will also allow pinned dependencies
+- `ignore`: The standard ignore list from the workspace root is also inherited
+
+**Important**: If a sub-package has its own `dart_dependency_validator.yaml` file, it will take complete precedence over the workspace configuration. The local config file is always prioritized over any inherited workspace settings.
diff --git a/example/example.md b/example/example.md
index 22ce7db..681d931 100644
--- a/example/example.md
+++ b/example/example.md
@@ -29,5 +29,17 @@ ignore:
- analyzer
# Allow dependencies to be pinned to a specific version instead of a range
-allowPins: true
+allow_pins: true
+
+# Workspace-specific configuration (only applies to workspace root packages):
+
+# Ignore specific packages in all workspace sub-packages
+workspace_global_ignore:
+ - some_package
+ - another_package
+
+# Skip validation for specific workspace packages
+workspace_package_ignore:
+ - pkg1
+ - pkg2
```
diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart
index 7eb7d9e..25d728f 100644
--- a/lib/src/dependency_validator.dart
+++ b/lib/src/dependency_validator.dart
@@ -27,7 +27,10 @@ import 'pubspec_config.dart';
import 'utils.dart';
/// Check for missing, under-promoted, over-promoted, and unused dependencies.
-Future checkPackage({required String root}) async {
+Future checkPackage(
+ {required String root,
+ List? inheritedWorkspaceGlobalIgnore,
+ bool inheritedAllowedPins = false}) async {
var result = true;
if (!File('$root/pubspec.yaml').existsSync()) {
logger.shout(red.wrap('pubspec.yaml not found'));
@@ -37,7 +40,9 @@ Future checkPackage({required String root}) async {
DepValidatorConfig config;
final configFile = File('$root/dart_dependency_validator.yaml');
+ var hasLocalFileConfig = false;
if (configFile.existsSync()) {
+ hasLocalFileConfig = true;
config = DepValidatorConfig.fromYaml(configFile.readAsStringSync());
} else {
final pubspecConfig = PubspecDepValidatorConfig.fromYaml(
@@ -66,7 +71,9 @@ Future checkPackage({required String root}) async {
.nonNulls
.toList();
logger.fine('excludes:\n${bulletItems(excludes.map((g) => g.pattern))}\n');
- final ignoredPackages = config.ignore;
+ final ignoredPackages = hasLocalFileConfig
+ ? config.ignore
+ : inheritedWorkspaceGlobalIgnore ?? config.ignore;
logger.fine('ignored packages:\n${bulletItems(ignoredPackages)}\n');
// Read and parse the analysis_options.yaml in the current working directory.
@@ -81,16 +88,25 @@ Future checkPackage({required String root}) async {
var subResult = true;
if (pubspec.isWorkspaceRoot) {
+ final workspacePackageIgnore = config.workspacePackageIgnore;
logger.fine('In a workspace. Recursing through sub-packages...');
for (final package in pubspec.workspace ?? []) {
- subResult &= await checkPackage(root: '$root/$package');
+ if (workspacePackageIgnore.contains(package)) {
+ logger.info('Skipping ${package} because it is ignored');
+ } else {
+ subResult &= await checkPackage(
+ root: '$root/$package',
+ inheritedWorkspaceGlobalIgnore: config.workspaceGlobalIgnore,
+ inheritedAllowedPins: config.allowPins);
+ }
logger.info('');
}
}
logger.info('Validating dependencies for ${pubspec.name}...');
-
- if (!config.allowPins) {
+ final allowedPins =
+ hasLocalFileConfig ? config.allowPins : inheritedAllowedPins;
+ if (!allowedPins) {
checkPubspecForPins(pubspec, ignoredPackages: ignoredPackages);
}
diff --git a/lib/src/pubspec_config.dart b/lib/src/pubspec_config.dart
index c9400a4..39a67c8 100644
--- a/lib/src/pubspec_config.dart
+++ b/lib/src/pubspec_config.dart
@@ -44,12 +44,20 @@ class DepValidatorConfig {
@JsonKey(defaultValue: [])
final List ignore;
+ @JsonKey(defaultValue: [])
+ final List workspaceGlobalIgnore;
+
+ @JsonKey(defaultValue: [])
+ final List workspacePackageIgnore;
+
@JsonKey(defaultValue: false)
final bool allowPins;
const DepValidatorConfig({
this.exclude = const [],
this.ignore = const [],
+ this.workspaceGlobalIgnore = const [],
+ this.workspacePackageIgnore = const [],
this.allowPins = false,
});
diff --git a/lib/src/pubspec_config.g.dart b/lib/src/pubspec_config.g.dart
index 5d9586b..3811889 100644
--- a/lib/src/pubspec_config.g.dart
+++ b/lib/src/pubspec_config.g.dart
@@ -12,35 +12,55 @@ PubspecDepValidatorConfig _$PubspecDepValidatorConfigFromJson(Map json) =>
json,
($checkedConvert) {
final val = PubspecDepValidatorConfig(
- dependencyValidator: $checkedConvert(
- 'dependency_validator',
- (v) => v == null ? null : DepValidatorConfig.fromJson(v as Map),
- ),
+ dependencyValidator: $checkedConvert('dependency_validator',
+ (v) => v == null ? null : DepValidatorConfig.fromJson(v as Map)),
);
return val;
},
fieldKeyMap: const {'dependencyValidator': 'dependency_validator'},
);
-DepValidatorConfig _$DepValidatorConfigFromJson(Map json) =>
- $checkedCreate('DepValidatorConfig', json, ($checkedConvert) {
- final val = DepValidatorConfig(
- exclude: $checkedConvert(
- 'exclude',
- (v) => (v as List?)?.map((e) => e as String).toList() ?? [],
- ),
- ignore: $checkedConvert(
- 'ignore',
- (v) => (v as List?)?.map((e) => e as String).toList() ?? [],
- ),
- allowPins: $checkedConvert('allow_pins', (v) => v as bool? ?? false),
- );
- return val;
- }, fieldKeyMap: const {'allowPins': 'allow_pins'});
+DepValidatorConfig _$DepValidatorConfigFromJson(Map json) => $checkedCreate(
+ 'DepValidatorConfig',
+ json,
+ ($checkedConvert) {
+ final val = DepValidatorConfig(
+ exclude: $checkedConvert(
+ 'exclude',
+ (v) =>
+ (v as List?)?.map((e) => e as String).toList() ??
+ []),
+ ignore: $checkedConvert(
+ 'ignore',
+ (v) =>
+ (v as List?)?.map((e) => e as String).toList() ??
+ []),
+ workspaceGlobalIgnore: $checkedConvert(
+ 'workspace_global_ignore',
+ (v) =>
+ (v as List?)?.map((e) => e as String).toList() ??
+ []),
+ workspacePackageIgnore: $checkedConvert(
+ 'workspace_package_ignore',
+ (v) =>
+ (v as List?)?.map((e) => e as String).toList() ??
+ []),
+ allowPins: $checkedConvert('allow_pins', (v) => v as bool? ?? false),
+ );
+ return val;
+ },
+ fieldKeyMap: const {
+ 'workspaceGlobalIgnore': 'workspace_global_ignore',
+ 'workspacePackageIgnore': 'workspace_package_ignore',
+ 'allowPins': 'allow_pins'
+ },
+ );
Map _$DepValidatorConfigToJson(DepValidatorConfig instance) =>
{
'exclude': instance.exclude,
'ignore': instance.ignore,
+ 'workspace_global_ignore': instance.workspaceGlobalIgnore,
+ 'workspace_package_ignore': instance.workspacePackageIgnore,
'allow_pins': instance.allowPins,
};
diff --git a/test/workspace_test.dart b/test/workspace_test.dart
index 8498d39..93113ed 100644
--- a/test/workspace_test.dart
+++ b/test/workspace_test.dart
@@ -149,4 +149,122 @@ void main() => group('Workspaces', () {
),
);
});
+
+ group('workspace_global_ignore', () {
+ test(
+ 'ignores packages in subpackages when set in workspace root',
+ () => checkWorkspace(
+ workspace: [],
+ workspaceDeps: {},
+ workspaceConfig: DepValidatorConfig(
+ workspaceGlobalIgnore: ['http'],
+ ),
+ subpackage: usesHttp,
+ subpackageDeps: {},
+ ),
+ );
+
+ test(
+ 'is inherited by subpackages without local config',
+ () => checkWorkspace(
+ workspace: usesHttp,
+ workspaceDeps: dependsOnHttp,
+ workspaceConfig: DepValidatorConfig(
+ workspaceGlobalIgnore: ['meta'],
+ ),
+ subpackage: usesMeta,
+ subpackageDeps: {},
+ ),
+ );
+
+ test(
+ 'does not apply when subpackage has its own config',
+ () => checkWorkspace(
+ workspace: [],
+ workspaceDeps: {},
+ workspaceConfig: DepValidatorConfig(
+ workspaceGlobalIgnore: ['http'],
+ ),
+ subpackage: usesHttp,
+ subpackageDeps: {},
+ subpackageConfig: DepValidatorConfig(ignore: []),
+ matcher: isFalse,
+ ),
+ );
+ });
+
+ group('workspace_package_ignore', () {
+ test(
+ 'skips validation for ignored workspace packages',
+ () => checkWorkspace(
+ workspace: [],
+ workspaceDeps: {},
+ workspaceConfig: DepValidatorConfig(
+ workspacePackageIgnore: ['subpackage'],
+ ),
+ subpackage: usesHttp,
+ subpackageDeps: {},
+ ),
+ );
+ });
+
+ group('allow_pins inheritance', () {
+ // Note: Pin checking doesn't affect the return value of checkPackage,
+ // it only sets exitCode. These tests verify configuration inheritance,
+ // while actual pin detection is tested in executable_test.dart
+
+ test(
+ 'workspace with explicit allow_pins configuration',
+ () => checkWorkspace(
+ workspace: usesHttp,
+ workspaceDeps: dependsOnHttp,
+ workspaceConfig: DepValidatorConfig(allowPins: true),
+ subpackage: usesMeta,
+ subpackageDeps: dependsOnMeta,
+ ),
+ );
+
+ test(
+ 'subpackage with local config can override workspace allow_pins',
+ () => checkWorkspace(
+ workspace: usesHttp,
+ workspaceDeps: dependsOnHttp,
+ workspaceConfig: DepValidatorConfig(allowPins: false),
+ subpackage: usesMeta,
+ subpackageDeps: dependsOnMeta,
+ subpackageConfig: DepValidatorConfig(allowPins: true),
+ ),
+ );
+ });
+
+ group('configuration precedence', () {
+ test(
+ 'local config ignore list takes precedence over workspace global ignore',
+ () => checkWorkspace(
+ workspace: [],
+ workspaceDeps: {},
+ workspaceConfig: DepValidatorConfig(
+ workspaceGlobalIgnore: ['http'],
+ ignore: ['meta'],
+ ),
+ subpackage: [...usesHttp, ...usesMeta],
+ subpackageDeps: {},
+ subpackageConfig: DepValidatorConfig(ignore: ['http', 'meta']),
+ ),
+ );
+
+ test(
+ 'workspace root uses its own ignore list, not workspace_global_ignore',
+ () => checkWorkspace(
+ workspace: usesHttp,
+ workspaceDeps: {},
+ workspaceConfig: DepValidatorConfig(
+ ignore: ['http'],
+ workspaceGlobalIgnore: ['meta'],
+ ),
+ subpackage: [],
+ subpackageDeps: {},
+ ),
+ );
+ });
});
From e092b5904d98e051e5af9a474eaa275e1aeedbfe Mon Sep 17 00:00:00 2001
From: Paul-Anatole CLAUDOT
Date: Thu, 19 Feb 2026 17:02:23 +0100
Subject: [PATCH 2/2] fix: add support of workspace glob (* and **)
---
lib/src/dependency_validator.dart | 20 ++++-
test/utils.dart | 67 ++++++++++++++
test/workspace_test.dart | 139 ++++++++++++++++++++++++++++++
3 files changed, 225 insertions(+), 1 deletion(-)
diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart
index 25d728f..33fc04f 100644
--- a/lib/src/dependency_validator.dart
+++ b/lib/src/dependency_validator.dart
@@ -90,7 +90,25 @@ Future checkPackage(
if (pubspec.isWorkspaceRoot) {
final workspacePackageIgnore = config.workspacePackageIgnore;
logger.fine('In a workspace. Recursing through sub-packages...');
- for (final package in pubspec.workspace ?? []) {
+
+ final subPackages = [];
+ for (final workspacePattern in pubspec.workspace ?? []) {
+ if (workspacePattern.contains('*')) {
+ final glob = makeGlob('$root/$workspacePattern');
+ final matchingDirs = Directory(root)
+ .listSync(recursive: true)
+ .whereType()
+ .where((dir) => glob.matches(dir.path))
+ .where((dir) => File('${dir.path}/pubspec.yaml').existsSync())
+ .map((dir) => p.relative(dir.path, from: root))
+ .toList();
+ subPackages.addAll(matchingDirs);
+ } else {
+ subPackages.add(workspacePattern);
+ }
+ }
+
+ for (final package in subPackages) {
if (workspacePackageIgnore.contains(package)) {
logger.info('Skipping ${package} because it is ignored');
} else {
diff --git a/test/utils.dart b/test/utils.dart
index 9b23add..cfef1da 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -114,3 +114,70 @@ Future checkWorkspace({
final result = await checkPackage(root: '${d.sandbox}/workspace');
expect(result, matcher);
}
+
+Future checkWorkspaceWithMultiplePackages({
+ required Map workspaceDeps,
+ required List workspacePatterns,
+ required Map subpackages,
+ required List workspace,
+ DepValidatorConfig? workspaceConfig,
+ Level logLevel = Level.OFF,
+ Matcher matcher = isTrue,
+}) async {
+ final workspacePubspec = Pubspec(
+ 'workspace',
+ environment: requireDart36,
+ dependencies: workspaceDeps,
+ workspace: workspacePatterns,
+ );
+
+ final subpackageDirs = [];
+ for (final entry in subpackages.entries) {
+ final packageName = entry.key;
+ final config = entry.value;
+ final subpackagePubspec = Pubspec(
+ packageName,
+ environment: requireDart36,
+ dependencies: config.dependencies,
+ resolution: 'workspace',
+ );
+ subpackageDirs.add(
+ d.dir(packageName, [
+ ...config.descriptors,
+ d.file('pubspec.yaml', jsonEncode(subpackagePubspec.toJson())),
+ if (config.config != null)
+ d.file(
+ 'dart_dependency_validator.yaml',
+ jsonEncode(config.config!.toJson()),
+ ),
+ ]),
+ );
+ }
+
+ final dir = d.dir('workspace', [
+ ...workspace,
+ d.file('pubspec.yaml', jsonEncode(workspacePubspec.toJson())),
+ if (workspaceConfig != null)
+ d.file(
+ 'dart_dependency_validator.yaml',
+ jsonEncode(workspaceConfig.toJson()),
+ ),
+ ...subpackageDirs,
+ ]);
+ await dir.create();
+ Logger.root.level = logLevel;
+ final result = await checkPackage(root: '${d.sandbox}/workspace');
+ expect(result, matcher);
+}
+
+class SubpackageConfig {
+ final Map dependencies;
+ final List descriptors;
+ final DepValidatorConfig? config;
+
+ SubpackageConfig({
+ required this.dependencies,
+ required this.descriptors,
+ this.config,
+ });
+}
diff --git a/test/workspace_test.dart b/test/workspace_test.dart
index 93113ed..29f1818 100644
--- a/test/workspace_test.dart
+++ b/test/workspace_test.dart
@@ -267,4 +267,143 @@ void main() => group('Workspaces', () {
),
);
});
+
+ group('glob patterns', () {
+ test(
+ 'handles single glob pattern matching multiple packages',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*'],
+ subpackages: {
+ 'packages/package_a': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ 'packages/package_b': SubpackageConfig(
+ dependencies: dependsOnMeta,
+ descriptors: usesMeta,
+ ),
+ },
+ ),
+ );
+
+ test(
+ 'handles nested glob pattern',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['modules/*/packages/*'],
+ subpackages: {
+ 'modules/core/packages/utils': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ 'modules/ui/packages/components': SubpackageConfig(
+ dependencies: dependsOnMeta,
+ descriptors: usesMeta,
+ ),
+ },
+ ),
+ );
+
+ test(
+ 'handles mix of glob patterns and literal paths',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*', 'tools/special_package'],
+ subpackages: {
+ 'packages/package_a': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ 'tools/special_package': SubpackageConfig(
+ dependencies: dependsOnMeta,
+ descriptors: usesMeta,
+ ),
+ },
+ ),
+ );
+
+ test(
+ 'ignores directories without pubspec.yaml when using glob',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*'],
+ subpackages: {
+ 'packages/package_with_pubspec': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ // packages/not_a_package directory will be created but without pubspec.yaml
+ },
+ ),
+ );
+
+ test(
+ 'fails when glob-matched subpackage has an issue',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*'],
+ subpackages: {
+ 'packages/package_a': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ 'packages/package_b': SubpackageConfig(
+ dependencies: {},
+ descriptors: usesHttp, // Uses http but doesn't declare it
+ ),
+ },
+ matcher: isFalse,
+ ),
+ );
+
+ test(
+ 'workspace_package_ignore works with glob patterns',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*'],
+ workspaceConfig: DepValidatorConfig(
+ workspacePackageIgnore: ['packages/package_b'],
+ ),
+ subpackages: {
+ 'packages/package_a': SubpackageConfig(
+ dependencies: dependsOnHttp,
+ descriptors: usesHttp,
+ ),
+ 'packages/package_b': SubpackageConfig(
+ dependencies: {},
+ descriptors: usesHttp, // Has issue but should be ignored
+ ),
+ },
+ ),
+ );
+
+ test(
+ 'workspace_global_ignore applies to glob-matched packages',
+ () => checkWorkspaceWithMultiplePackages(
+ workspace: [],
+ workspaceDeps: {},
+ workspacePatterns: ['packages/*'],
+ workspaceConfig: DepValidatorConfig(
+ workspaceGlobalIgnore: ['http'],
+ ),
+ subpackages: {
+ 'packages/package_a': SubpackageConfig(
+ dependencies: {},
+ descriptors: usesHttp, // Uses http but it's globally ignored
+ ),
+ 'packages/package_b': SubpackageConfig(
+ dependencies: dependsOnMeta,
+ descriptors: usesMeta,
+ ),
+ },
+ ),
+ );
+ });
});