Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 5.0.3

- Add YAML list format support for analysis_options file

# 5.0.2

- Fixed changelog entries caused from mis-configured auto-release
Expand Down
56 changes: 29 additions & 27 deletions lib/src/dependency_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,17 @@ Future<bool> checkPackage({required String root}) async {
config = pubspecConfig.dependencyValidator;
}

final excludes =
config.exclude
.map((s) {
try {
return makeGlob("$root/$s");
} catch (_, __) {
logger.shout(yellow.wrap('invalid glob syntax: "$s"'));
return null;
}
})
.nonNulls
.toList();
final excludes = config.exclude
.map((s) {
try {
return makeGlob("$root/$s");
} catch (_, __) {
logger.shout(yellow.wrap('invalid glob syntax: "$s"'));
return null;
}
})
.nonNulls
.toList();
logger.fine('excludes:\n${bulletItems(excludes.map((g) => g.pattern))}\n');
final ignoredPackages = config.ignore;
logger.fine('ignored packages:\n${bulletItems(ignoredPackages)}\n');
Expand Down Expand Up @@ -199,7 +198,7 @@ Future<bool> checkPackage({required String root}) async {
final packagesUsedOutsidePublicDirs = <String>{
// For more info on analysis options:
// https://dart.dev/guides/language/analysis-options#the-analysis-options-file
if (optionsIncludePackage != null) optionsIncludePackage,
if (optionsIncludePackage != null) ...optionsIncludePackage,
};
for (final file in nonPublicDartFiles) {
packagesUsedOutsidePublicDirs.addAll(getDartDirectivePackageNames(file));
Expand Down Expand Up @@ -268,13 +267,13 @@ Future<bool> checkPackage({required String root}) async {
// Packages that are not used in lib/, but are used elsewhere, that are
// dependencies when they should be dev_dependencies.
final overPromotedDependencies =
// Start with dependencies that are not used in lib/
(deps
.difference(packagesUsedInPublicFiles)
// Intersect with deps that are used outside lib/ (excludes unused deps)
.intersection(packagesUsedOutsidePublicDirs))
// Ignore known over-promoted packages.
..removeAll(ignoredPackages);
// Start with dependencies that are not used in lib/
(deps
.difference(packagesUsedInPublicFiles)
// Intersect with deps that are used outside lib/ (excludes unused deps)
.intersection(packagesUsedOutsidePublicDirs))
// Ignore known over-promoted packages.
..removeAll(ignoredPackages);

if (overPromotedDependencies.isNotEmpty) {
log(
Expand All @@ -287,10 +286,10 @@ Future<bool> checkPackage({required String root}) async {

// Packages that are used in lib/, but are dev_dependencies.
final underPromotedDependencies =
// Start with dev_dependencies that are used in lib/
devDeps.intersection(packagesUsedInPublicFiles)
// Ignore known under-promoted packages
..removeAll(ignoredPackages);
// Start with dev_dependencies that are used in lib/
devDeps.intersection(packagesUsedInPublicFiles)
// Ignore known under-promoted packages
..removeAll(ignoredPackages);

if (underPromotedDependencies.isNotEmpty) {
log(
Expand Down Expand Up @@ -334,8 +333,8 @@ Future<bool> checkPackage({required String root}) async {
for (final target in rootBuildConfig.buildTargets.values)
...target.builders.keys,
]
.map((key) => normalizeBuilderKeyUsage(key, pubspec.name))
.any((key) => key.startsWith('$dependencyName:'));
.map((key) => normalizeBuilderKeyUsage(key, pubspec.name))
.any((key) => key.startsWith('$dependencyName:'));

final packagesWithConsumedBuilders = Set<String>();
for (final name in unusedDependencies) {
Expand Down Expand Up @@ -420,7 +419,10 @@ Future<bool> checkPackage({required String root}) async {
Future<bool> dependencyDefinesAutoAppliedBuilder(String path) async =>
(await BuildConfig.fromPackageDir(
path,
)).builderDefinitions.values.any((def) => def.autoApply != AutoApply.none);
))
.builderDefinitions
.values
.any((def) => def.autoApply != AutoApply.none);

/// Checks for dependency pins.
///
Expand Down
38 changes: 32 additions & 6 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,45 @@ final Logger logger = Logger('dependency_validator');
String bulletItems(Iterable<String> items) =>
items.map((l) => ' * $l').join('\n');

/// Returns the name of the package referenced in the `include:` directive in an
/// analysis_options.yaml file, or null if there is not one.
String? getAnalysisOptionsIncludePackage({String? path}) {
/// Returns the names of the packages referenced in the `include:` directive in an
/// analysis_options.yaml file, or null if there are none.
///
/// Supports both single string includes:
/// include: package:flutter_lints/flutter.yaml
///
/// And list includes:
/// include:
/// - package:flutter_lints/flutter.yaml
/// - package:another_package/config.yaml
Set<String>? getAnalysisOptionsIncludePackage({String? path}) {
final optionsFile = File(p.join(path ?? p.current, 'analysis_options.yaml'));
if (!optionsFile.existsSync()) return null;

final YamlMap? analysisOptions = loadYaml(optionsFile.readAsStringSync());
if (analysisOptions == null) return null;

final String? include = analysisOptions['include'];
if (include == null || !include.startsWith('package:')) return null;
final Object? include = analysisOptions['include'];
if (include == null) return null;

final packageNames = <String>{};

if (include is String) {
// Handle single string include
if (include.startsWith('package:')) {
final packageName = Uri.parse(include).pathSegments.first;
packageNames.add(packageName);
}
} else if (include is YamlList) {
// Handle list of includes
for (final item in include) {
if (item is String && item.startsWith('package:')) {
final packageName = Uri.parse(item).pathSegments.first;
packageNames.add(packageName);
}
}
}

return Uri.parse(include).pathSegments.first;
return packageNames.isEmpty ? null : packageNames;
}

/// Returns an iterable of all Dart files (files ending in .dart) in the given
Expand Down
39 changes: 38 additions & 1 deletion test/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,44 @@ linter:
await d.file('analysis_options.yaml', '''
include: package:pedantic/analysis_options.1.8.0.yaml
''').create();
expect(getAnalysisOptionsIncludePackage(path: d.sandbox), 'pedantic');
expect(getAnalysisOptionsIncludePackage(path: d.sandbox), {'pedantic'});
});

test('returns package names from list `include:`', () async {
await d.file('analysis_options.yaml', '''
include:
- package:flutter_lints/flutter.yaml
- package:pedantic/analysis_options.1.8.0.yaml
''').create();
expect(getAnalysisOptionsIncludePackage(path: d.sandbox),
{'flutter_lints', 'pedantic'});
});

test('filters out non-package includes from list', () async {
await d.file('analysis_options.yaml', '''
include:
- package:flutter_lints/flutter.yaml
- analysis_options_shared.yaml
- package:pedantic/analysis_options.1.8.0.yaml
''').create();
expect(getAnalysisOptionsIncludePackage(path: d.sandbox),
{'flutter_lints', 'pedantic'});
});

test('returns null for list with no package includes', () async {
await d.file('analysis_options.yaml', '''
include:
- analysis_options_shared.yaml
- ../common_options.yaml
''').create();
expect(getAnalysisOptionsIncludePackage(path: d.sandbox), isNull);
});

test('handles empty list', () async {
await d.file('analysis_options.yaml', '''
include: []
''').create();
expect(getAnalysisOptionsIncludePackage(path: d.sandbox), isNull);
});
});

Expand Down