diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed194e..2ee6bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 4fe5ed1..a23e871 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -54,18 +54,17 @@ Future 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'); @@ -199,7 +198,7 @@ Future checkPackage({required String root}) async { final packagesUsedOutsidePublicDirs = { // 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)); @@ -268,13 +267,13 @@ Future 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( @@ -287,10 +286,10 @@ Future 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( @@ -334,8 +333,8 @@ Future 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(); for (final name in unusedDependencies) { @@ -420,7 +419,10 @@ Future checkPackage({required String root}) async { Future 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. /// diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 427a5f5..342a202 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -31,19 +31,45 @@ final Logger logger = Logger('dependency_validator'); String bulletItems(Iterable 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? 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 = {}; + + 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 diff --git a/test/utils_test.dart b/test/utils_test.dart index 4c7407c..bc40e78 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -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); }); });