Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased
<!-- Add unreleased changes here -->
- Ignore nested packages when validating surrounding packages (#173).

# 5.0.5

Expand Down
41 changes: 25 additions & 16 deletions lib/src/dependency_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Future<bool> checkPackage({required String root}) async {
.map((s) {
try {
return makeGlob("$root/$s");
} catch (_, __) {
} catch (_) {
logger.shout(yellow.wrap('invalid glob syntax: "$s"'));
return null;
}
Expand Down Expand Up @@ -105,16 +105,31 @@ Future<bool> checkPackage({required String root}) async {
'${bulletItems(devDeps)}\n',
);

final nestedPackages = listNestedPackages(root);
final nestedPackageGlobs = [
for (final nested in nestedPackages)
makeGlob('${p.normalize(nested.path)}/**'),
for (final subpackage in pubspec.workspace ?? [])
makeGlob('${p.normalize('$root/$subpackage')}/**'),
];
logger.fine(
'nested package globs:\n'
'${bulletItems(nestedPackageGlobs.map((g) => g.pattern))}\n',
);

final publicDirs = ['$root/bin/', '$root/lib/'];
logger.fine("Excluding: $excludes");
final publicDartFiles = [
for (final dir in publicDirs) ...listDartFilesIn(dir, excludes),
for (final dir in publicDirs)
...listDartFilesIn(dir, [...excludes, ...nestedPackageGlobs]),
];
final publicScssFiles = [
for (final dir in publicDirs) ...listScssFilesIn(dir, excludes),
for (final dir in publicDirs)
...listScssFilesIn(dir, [...excludes, ...nestedPackageGlobs]),
];
final publicLessFiles = [
for (final dir in publicDirs) ...listLessFilesIn(dir, excludes),
for (final dir in publicDirs)
...listLessFilesIn(dir, [...excludes, ...nestedPackageGlobs]),
];

logger
Expand Down Expand Up @@ -156,27 +171,20 @@ Future<bool> checkPackage({required String root}) async {

final publicDirGlobs = [for (final dir in publicDirs) makeGlob('$dir**')];

final subpackageGlobs = [
for (final subpackage in pubspec.workspace ?? [])
makeGlob('$root/$subpackage**'),
];

logger.fine('subpackage globs: $subpackageGlobs');

final nonPublicDartFiles = listDartFilesIn('$root/', [
...excludes,
...publicDirGlobs,
...subpackageGlobs,
...nestedPackageGlobs,
]);
final nonPublicScssFiles = listScssFilesIn('$root/', [
...excludes,
...publicDirGlobs,
...subpackageGlobs,
...nestedPackageGlobs,
]);
final nonPublicLessFiles = listLessFilesIn('$root/', [
...excludes,
...publicDirGlobs,
...subpackageGlobs,
...nestedPackageGlobs,
]);

logger
Expand Down Expand Up @@ -309,8 +317,7 @@ Future<bool> checkPackage({required String root}) async {
.difference(packagesUsedInPublicFiles)
.difference(packagesUsedOutsidePublicDirs)
// Remove this package, since we know they're using our executable
..remove(dependencyValidatorPackageName)
..removeAll(ignoredPackages);
..remove(dependencyValidatorPackageName);

final packageConfig = await findPackageConfig(Directory.current);
if (packageConfig == null) {
Expand Down Expand Up @@ -400,6 +407,8 @@ Future<bool> checkPackage({required String root}) async {
);
}

unusedDependencies.removeAll(ignoredPackages);

if (unusedDependencies.isNotEmpty) {
log(
Level.WARNING,
Expand Down
21 changes: 21 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ Iterable<File> listFilesWithExtensionIn(
.where((file) => excludes.every((glob) => !glob.matches(file.path)));
}

/// Returns an iterable of all directories containing a `pubspec.yaml` file
/// within [dirPath], excluding [dirPath] itself.
///
/// This also excludes directories inside hidden directories, like `.dart_tool/`.
Iterable<Directory> listNestedPackages(String dirPath) {
final rootDir = Directory(dirPath);
if (!rootDir.existsSync()) return [];

final rootCanonicalPath = p.canonicalize(rootDir.path);

return rootDir
.listSync(recursive: true)
.whereType<File>()
.where(
(file) => !p.split(file.path).any((d) => d != '.' && d.startsWith('.')),
)
.where((file) => p.basename(file.path) == 'pubspec.yaml')
.map((file) => file.parent)
.where((dir) => p.canonicalize(dir.path) != rootCanonicalPath);
}

/// Logs the given [message] at [level] and lists all of the given [dependencies].
void log(Level level, String message, Iterable<String> dependencies) {
final sortedDependencies = dependencies.toList()..sort();
Expand Down
9 changes: 4 additions & 5 deletions test/executable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ void main() {
devDependencies: {
"build_runner": hostedCompatibleWith('2.3.3'),
'coverage': hostedAny,
'dart_style': hostedCompatibleWith('2.3.2'),
'dart_style': hostedAny,
},
project: [
d.dir('lib', [d.file('main.dart', 'book fake = true;')]),
Expand All @@ -331,7 +331,7 @@ void main() {
dependencies: {
"build_runner": hostedCompatibleWith('2.3.3'),
"coverage": hostedAny,
"dart_style": hostedCompatibleWith('2.3.2'),
"dart_style": hostedAny,
},
project: [
d.dir('lib', [d.file('main.dart', 'bool fake = true;')]),
Expand All @@ -353,9 +353,8 @@ void main() {
() async {
result = await checkProject(
devDependencies: {
'build_test': hostedCompatibleWith('2.0.1'),
'build_vm_compilers': hostedCompatibleWith('1.0.3'),
'build_web_compilers': hostedCompatibleWith('3.2.7'),
'build_test': hostedAny,
'build_web_compilers': hostedAny,
},
project: [
d.dir('lib', [d.file('main.dart', 'book fake = true;')]),
Expand Down
206 changes: 206 additions & 0 deletions test/nested_packages_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import 'dart:convert';
import 'package:dependency_validator/src/dependency_validator.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import 'pubspec_to_json.dart';
import 'utils.dart';

void main() => group('Nested packages', () {
initLogs();

test('ignores dependencies used only in nested packages', () async {
final rootPubspec = Pubspec(
'code_assets',
environment: requireDart36,
dependencies: {
'http': HostedDependency(version: VersionConstraint.any),
},
);

final nestedPubspec = Pubspec(
'host_name',
environment: requireDart36,
devDependencies: {
'ffigen': HostedDependency(version: VersionConstraint.any),
},
);

final dir = d.dir('code_assets', [
d.file('pubspec.yaml', jsonEncode(rootPubspec.toJson())),
d.dir('lib', [
d.file('code_assets.dart', 'import "package:http/http.dart";'),
]),
d.dir('example', [
d.dir('host_name', [
d.file('pubspec.yaml', jsonEncode(nestedPubspec.toJson())),
d.dir('tool', [
d.file('ffigen.dart', 'import "package:ffigen/ffigen.dart";'),
]),
d.dir('lib', [
d.file(
'host_name.dart', 'import "package:archive/archive.dart";'),
]),
]),
]),
]);

await dir.create();
final result = await checkPackage(root: '${d.sandbox}/code_assets');
expect(result, isTrue);
});

test(
'fails when root package itself has undeclared dependencies outside nested packages',
() async {
final rootPubspec = Pubspec(
'code_assets',
environment: requireDart36,
dependencies: {},
);

final nestedPubspec = Pubspec(
'host_name',
environment: requireDart36,
devDependencies: {
'ffigen': HostedDependency(version: VersionConstraint.any),
},
);

final dir = d.dir('code_assets_with_issue', [
d.file('pubspec.yaml', jsonEncode(rootPubspec.toJson())),
d.dir('tool', [
// Undeclared dependency in root package's own tool dir
d.file('root_tool.dart', 'import "package:meta/meta.dart";'),
]),
d.dir('example', [
d.dir('host_name', [
d.file('pubspec.yaml', jsonEncode(nestedPubspec.toJson())),
d.dir('tool', [
d.file('ffigen.dart', 'import "package:ffigen/ffigen.dart";'),
]),
]),
]),
]);

await dir.create();
final result =
await checkPackage(root: '${d.sandbox}/code_assets_with_issue');
expect(result, isFalse);
});

test('ignores deeply nested packages', () async {
final rootPubspec = Pubspec(
'root_pkg',
environment: requireDart36,
);

final deeplyNestedPubspec = Pubspec(
'deep_pkg',
environment: requireDart36,
);

final dir = d.dir('root_pkg', [
d.file('pubspec.yaml', jsonEncode(rootPubspec.toJson())),
d.dir('example', [
d.dir('nested', [
d.dir('deep', [
d.file(
'pubspec.yaml', jsonEncode(deeplyNestedPubspec.toJson())),
d.dir('lib', [
d.file('deep.dart', 'import "package:meta/meta.dart";'),
]),
]),
]),
]),
]);

await dir.create();
final result = await checkPackage(root: '${d.sandbox}/root_pkg');
expect(result, isTrue);
});

test('ignores SCSS and Less files in nested packages', () async {
final rootPubspec = Pubspec(
'web_pkg',
environment: requireDart36,
);

final nestedPubspec = Pubspec(
'nested_web_pkg',
environment: requireDart36,
);

final dir = d.dir('web_pkg', [
d.file('pubspec.yaml', jsonEncode(rootPubspec.toJson())),
d.dir('example', [
d.dir('nested_web', [
d.file('pubspec.yaml', jsonEncode(nestedPubspec.toJson())),
d.dir('web', [
d.file(
'style.scss', '@import "package:foo_styles/style.scss";'),
d.file(
'style.less', '@import "packages/bar_styles/style.less";'),
]),
]),
]),
]);

await dir.create();
final result = await checkPackage(root: '${d.sandbox}/web_pkg');
expect(result, isTrue);
});

test('works with workspace subpackages that contain nested packages',
() async {
final workspacePubspec = Pubspec(
'workspace_root',
environment: requireDart36,
workspace: ['pkgs/code_assets'],
);

final subpackagePubspec = Pubspec(
'code_assets',
environment: requireDart36,
resolution: 'workspace',
dependencies: {
'http': HostedDependency(version: VersionConstraint.any),
},
);

final nestedPubspec = Pubspec(
'host_name',
environment: requireDart36,
dependencies: {
'ffigen': HostedDependency(version: VersionConstraint.any),
},
);

final dir = d.dir('workspace', [
d.file('pubspec.yaml', jsonEncode(workspacePubspec.toJson())),
d.dir('pkgs', [
d.dir('code_assets', [
d.file('pubspec.yaml', jsonEncode(subpackagePubspec.toJson())),
d.dir('lib', [
d.file('code_assets.dart', 'import "package:http/http.dart";'),
]),
d.dir('example', [
d.dir('host_name', [
d.file('pubspec.yaml', jsonEncode(nestedPubspec.toJson())),
d.dir('tool', [
d.file(
'ffigen.dart', 'import "package:ffigen/ffigen.dart";'),
]),
]),
]),
]),
]),
]);

await dir.create();
final result = await checkPackage(root: '${d.sandbox}/workspace');
expect(result, isTrue);
});
});
Loading