From 2a52593d6814f9e4c7229ee48587f8f2d2b74212 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 19:16:41 +0000 Subject: [PATCH 1/8] FEDX-7267: support Dart doc imports in dependency_validator Co-authored-by: Dustin Pauze --- lib/src/dependency_validator.dart | 17 +++- lib/src/import_export_ast_visitor.dart | 49 ++++++++-- test/executable_test.dart | 116 +++++++++++++++++++++++ test/import_export_ast_visitor_test.dart | 93 ++++++++++++++++++ 4 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 test/import_export_ast_visitor_test.dart diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 7eb7d9e..4617918 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -131,11 +131,14 @@ Future checkPackage({required String root}) async { '${bulletItems(publicLessFiles.map((f) => f.path))}\n', ); - // Read each file in lib/ and parse the package names from every import and - // export directive. + // Read each file in lib/ and parse the package names from every import, + // export directive, and doc import. final packagesUsedInPublicFiles = {}; + final packagesUsedViaDocImportInPublicFiles = {}; for (final file in publicDartFiles) { - packagesUsedInPublicFiles.addAll(getDartDirectivePackageNames(file)); + final usage = getDartPackageUsage(file); + packagesUsedInPublicFiles.addAll(usage.directivePackageNames); + packagesUsedViaDocImportInPublicFiles.addAll(usage.docImportPackageNames); } for (final file in publicScssFiles) { final matches = importScssPackageRegex.allMatches(file.readAsStringSync()); @@ -201,8 +204,14 @@ Future checkPackage({required String root}) async { if (optionsIncludePackage != null) optionsIncludePackage, }; for (final file in nonPublicDartFiles) { - packagesUsedOutsidePublicDirs.addAll(getDartDirectivePackageNames(file)); + final usage = getDartPackageUsage(file); + packagesUsedOutsidePublicDirs.addAll(usage.directivePackageNames); + packagesUsedOutsidePublicDirs.addAll(usage.docImportPackageNames); } + + // Doc imports in lib/ are not runtime dependencies, so treat them like usage + // outside lib/ for dependency promotion checks. + packagesUsedOutsidePublicDirs.addAll(packagesUsedViaDocImportInPublicFiles); for (final file in nonPublicScssFiles) { final matches = importScssPackageRegex.allMatches(file.readAsStringSync()); for (final match in matches) { diff --git a/lib/src/import_export_ast_visitor.dart b/lib/src/import_export_ast_visitor.dart index 5eb82d1..a1280b8 100644 --- a/lib/src/import_export_ast_visitor.dart +++ b/lib/src/import_export_ast_visitor.dart @@ -5,9 +5,23 @@ import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -/// Returns the list of package names that are exported and imported into the -/// provided dart file -Set getDartDirectivePackageNames(File file) { +/// Package names referenced in a Dart file via import/export directives and +/// doc imports. +class DartPackageUsage { + /// Package names from `import` and `export` directives. + final Set directivePackageNames; + + /// Package names from `@docImport` documentation imports. + final Set docImportPackageNames; + + const DartPackageUsage({ + required this.directivePackageNames, + required this.docImportPackageNames, + }); +} + +/// Returns the package names referenced in the provided Dart file. +DartPackageUsage getDartPackageUsage(File file) { ParseStringResult parsed; try { parsed = parseString(content: file.readAsStringSync(), path: file.path); @@ -18,26 +32,41 @@ Set getDartDirectivePackageNames(File file) { } final visitor = ImportExportVisitor(); - parsed.unit.visitChildren(visitor); - return visitor.packageNames; + parsed.unit.accept(visitor); + return DartPackageUsage( + directivePackageNames: visitor.directivePackageNames, + docImportPackageNames: visitor.docImportPackageNames, + ); } class ImportExportVisitor extends GeneralizingAstVisitor { - Set packageNames = {}; + Set directivePackageNames = {}; + Set docImportPackageNames = {}; @override void visitDirective(Directive node) { - if (node is! UriBasedDirective) return; + if (node is UriBasedDirective) { + _addPackageName(node.uri.stringValue, directivePackageNames); + } + super.visitDirective(node); + } + + @override + void visitComment(Comment node) { + for (final docImport in node.docImports) { + _addPackageName(docImport.import.uri.stringValue, docImportPackageNames); + } + super.visitComment(node); + } - final uri = node.uri.stringValue; + void _addPackageName(String? uri, Set packageNames) { if (uri == null) return; // ignore relative path imports if (!uri.startsWith('package:')) return; final packageParts = uri.substring('package:'.length).split('/'); - if (packageParts.isEmpty) - return; // sanity check, this probably will never happen + if (packageParts.isEmpty) return; packageNames.add(packageParts.first); } diff --git a/test/executable_test.dart b/test/executable_test.dart index 71f9b47..df77e23 100644 --- a/test/executable_test.dart +++ b/test/executable_test.dart @@ -261,6 +261,122 @@ void main() { ); }); + group('doc imports', () { + test('passes when a dev_dependency is only referenced via doc import in lib/', () async { + result = await checkProject( + devDependencies: {'meta': hostedAny}, + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]), + ], + ); + + expect(result.exitCode, 0); + expect(result.stdout, contains('No dependency issues found!')); + }); + + test( + 'fails when a package referenced via doc import in lib/ is missing from pubspec', + () async { + result = await checkProject( + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]), + ], + ); + + expect(result.exitCode, 1); + expect( + result.stderr, + contains( + 'These packages are used outside lib/ but are not dev_dependencies:', + ), + ); + expect(result.stderr, contains('meta')); + }, + ); + + test( + 'fails when a dependency is only referenced via doc import in lib/', + () async { + result = await checkProject( + dependencies: {'meta': hostedAny}, + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]), + ], + ); + + expect(result.exitCode, 1); + expect( + result.stderr, + contains( + 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', + ), + ); + expect(result.stderr, contains('meta')); + }, + ); + + test('does not flag doc-import-only packages as unused', () async { + result = await checkProject( + devDependencies: {'meta': hostedAny}, + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]), + ], + ); + + expect(result.exitCode, 0); + expect( + result.stderr, + isNot(contains( + 'These packages may be unused, or you may be using assets from these packages:', + )), + ); + }); + }); + test( 'warns when the analyzer package is depended on but not used', () async { diff --git a/test/import_export_ast_visitor_test.dart b/test/import_export_ast_visitor_test.dart new file mode 100644 index 0000000..d9f20be --- /dev/null +++ b/test/import_export_ast_visitor_test.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:dependency_validator/src/import_export_ast_visitor.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + group('getDartPackageUsage', () { + test('collects import and export directives', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +import 'package:logging/logging.dart'; +export 'package:meta/meta.dart'; +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, {'logging', 'meta'}); + expect(usage.docImportPackageNames, isEmpty); + }); + + test('collects doc imports from documentation comments', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, {'meta'}); + }); + + test('collects both directives and doc imports', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:yaml/yaml.dart'; +library; + +import 'package:logging/logging.dart'; + +/// References [YamlMap]. +class Foo {} +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, {'logging'}); + expect(usage.docImportPackageNames, {'yaml'}); + }); + + test('ignores relative and dart scheme imports', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'dart:async'; +import 'other.dart'; +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, isEmpty); + }); + }); +} From ed85ed3e668741c36835f663261b0adfd014d6cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 19:33:27 +0000 Subject: [PATCH 2/8] FEDX-7267: address PR review feedback for doc import support - Exclude doc-import-only lib/ packages from over-promotion checks - Bump Dart SDK to 3.8.1 in CI, tool-versions, and pubspec (^3.8.0) - Pass explicit FeatureSet 3.8.0 to parseString for doc imports - Add requireDart38 test fixture environment for doc import tests - Expand AST visitor tests (declaration comments, show/as, bin/) - Run dart format and bump version to 5.1.0 with CHANGELOG entry Co-authored-by: Dustin Pauze --- .github/workflows/ci.yaml | 6 +- .tool-versions | 2 +- CHANGELOG.md | 11 +++ lib/src/dependency_validator.dart | 3 + lib/src/import_export_ast_visitor.dart | 11 ++- pubspec.yaml | 4 +- test/executable_test.dart | 49 ++++++------- test/import_export_ast_visitor_test.dart | 87 ++++++++++++++++++++++++ test/utils.dart | 7 +- 9 files changed, 148 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9e064a3..a6ab38c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,14 +10,14 @@ jobs: build: uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.14 with: - sdk: 3.7.2 # mirrors .tool-versions + sdk: 3.8.1 # mirrors .tool-versions checks: uses: Workiva/gha-dart-oss/.github/workflows/checks.yaml@v0.1.14 with: - sdk: 3.7.2 # mirrors .tool-versions + sdk: 3.8.1 # mirrors .tool-versions unit-tests: uses: Workiva/gha-dart-oss/.github/workflows/test-unit.yaml@v0.1.14 with: - sdk: 3.7.2 # mirrors .tool-versions + sdk: 3.8.1 # mirrors .tool-versions diff --git a/.tool-versions b/.tool-versions index ed696fb..afa1372 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -dart 3.7.2 +dart 3.8.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b5bbd..73cb239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Unreleased +# 5.1.0 + +- Added support for Dart `@docImport` documentation imports when scanning + package usage. +- Packages referenced only via `@docImport` in `lib/` are accepted as either + `dependencies` or `dev_dependencies` and are not flagged as over-promoted, + missing, or unused. +- Removed the internal `getDartDirectivePackageNames` API in favor of + `getDartPackageUsage`. +- Requires Dart 3.8 or above. + # 5.0.6 - Allow up to analyzer 13 diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 4617918..93d61e0 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -281,6 +281,9 @@ Future checkPackage({required String root}) async { .difference(packagesUsedInPublicFiles) // Intersect with deps that are used outside lib/ (excludes unused deps) .intersection(packagesUsedOutsidePublicDirs)) + // Doc imports in lib/ are not runtime deps; accept either dependencies or + // dev_dependencies without flagging over-promotion. + ..removeAll(packagesUsedViaDocImportInPublicFiles) // Ignore known over-promoted packages. ..removeAll(ignoredPackages); diff --git a/lib/src/import_export_ast_visitor.dart b/lib/src/import_export_ast_visitor.dart index a1280b8..0de8d20 100644 --- a/lib/src/import_export_ast_visitor.dart +++ b/lib/src/import_export_ast_visitor.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:pub_semver/pub_semver.dart'; /// Package names referenced in a Dart file via import/export directives and /// doc imports. @@ -24,7 +26,14 @@ class DartPackageUsage { DartPackageUsage getDartPackageUsage(File file) { ParseStringResult parsed; try { - parsed = parseString(content: file.readAsStringSync(), path: file.path); + parsed = parseString( + content: file.readAsStringSync(), + path: file.path, + featureSet: FeatureSet.fromEnableFlags2( + sdkLanguageVersion: Version.parse('3.8.0'), + flags: const [], + ), + ); } on ArgumentError catch (e) { print('Error parsing: ${file.path}'); print(e.message); diff --git a/pubspec.yaml b/pubspec.yaml index 3420e1a..65953e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: dependency_validator -version: 5.0.6 +version: 5.1.0 description: Checks for missing, under-promoted, over-promoted, and unused dependencies. homepage: https://github.com/Workiva/dependency_validator environment: - sdk: ^3.0.0 + sdk: ^3.8.0 dependencies: analyzer: ">=7.1.0 <15.0.0" diff --git a/test/executable_test.dart b/test/executable_test.dart index df77e23..6810bf7 100644 --- a/test/executable_test.dart +++ b/test/executable_test.dart @@ -262,33 +262,38 @@ void main() { }); group('doc imports', () { - test('passes when a dev_dependency is only referenced via doc import in lib/', () async { - result = await checkProject( - devDependencies: {'meta': hostedAny}, - project: [ - d.dir('lib', [ - d.file( - 'main.dart', - ''' + test( + 'passes when a dev_dependency is only referenced via doc import in lib/', + () async { + result = await checkProject( + devDependencies: {'meta': hostedAny}, + environment: requireDart38, + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} ''', - ), - ]), - ], - ); + ), + ]), + ], + ); - expect(result.exitCode, 0); - expect(result.stdout, contains('No dependency issues found!')); - }); + expect(result.exitCode, 0); + expect(result.stdout, contains('No dependency issues found!')); + }, + ); test( 'fails when a package referenced via doc import in lib/ is missing from pubspec', () async { result = await checkProject( + environment: requireDart38, project: [ d.dir('lib', [ d.file( @@ -317,10 +322,11 @@ class Foo {} ); test( - 'fails when a dependency is only referenced via doc import in lib/', + 'passes when a dependency is only referenced via doc import in lib/', () async { result = await checkProject( dependencies: {'meta': hostedAny}, + environment: requireDart38, project: [ d.dir('lib', [ d.file( @@ -337,20 +343,15 @@ class Foo {} ], ); - expect(result.exitCode, 1); - expect( - result.stderr, - contains( - 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', - ), - ); - expect(result.stderr, contains('meta')); + expect(result.exitCode, 0); + expect(result.stdout, contains('No dependency issues found!')); }, ); test('does not flag doc-import-only packages as unused', () async { result = await checkProject( devDependencies: {'meta': hostedAny}, + environment: requireDart38, project: [ d.dir('lib', [ d.file( diff --git a/test/import_export_ast_visitor_test.dart b/test/import_export_ast_visitor_test.dart index d9f20be..fbfd47f 100644 --- a/test/import_export_ast_visitor_test.dart +++ b/test/import_export_ast_visitor_test.dart @@ -47,6 +47,30 @@ class Foo {} expect(usage.docImportPackageNames, {'meta'}); }); + test( + 'collects doc imports from declaration doc comments without a library directive', + () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; + +/// References [Deprecated]. +class Foo {} +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, {'meta'}); + }, + ); + test('collects both directives and doc imports', () async { await d.dir('project', [ d.file( @@ -71,6 +95,69 @@ class Foo {} expect(usage.docImportPackageNames, {'yaml'}); }); + test('collects package names from doc imports with show clauses', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:collection/collection.dart' show IterableExtension; + +/// References [IterableExtension]. +class Foo {} +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.docImportPackageNames, {'collection'}); + }); + + test('collects package names from doc imports with as clauses', () async { + await d.dir('project', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:collection/collection.dart' as collection; + +/// References [collection.IterableExtension]. +class Foo {} +''', + ), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.docImportPackageNames, {'collection'}); + }); + + test('collects doc imports from bin/ files', () async { + await d.dir('project', [ + d.dir('bin', [ + d.file( + 'main.dart', + ''' +/// @docImport 'package:meta/meta.dart'; + +/// References [Deprecated]. +void main() {} +''', + ), + ]), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/bin/main.dart'), + ); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, {'meta'}); + }); + test('ignores relative and dart scheme imports', () async { await d.dir('project', [ d.file( diff --git a/test/utils.dart b/test/utils.dart index 9b23add..febd158 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -20,10 +20,11 @@ Future checkProject({ List project = const [], List args = const [], bool embedConfigInPubspec = false, + Map? environment, }) async { final pubspec = Pubspec( 'project', - environment: requireDart36, + environment: environment ?? requireDart36, dependencies: dependencies, devDependencies: { ...devDependencies, @@ -69,6 +70,10 @@ final requireDart36 = { "sdk": VersionConstraint.compatibleWith(Version.parse('3.6.0')), }; +final requireDart38 = { + "sdk": VersionConstraint.compatibleWith(Version.parse('3.8.0')), +}; + Future checkWorkspace({ required Map workspaceDeps, required Map subpackageDeps, From 8562a8b07b78eafb85f80c22bf7c4099e2e6a4c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 19:36:24 +0000 Subject: [PATCH 3/8] FEDX-7267: fix CI format and analyze failures - Run dart format across the repo with Dart 3.8.1 - Remove obsolete test/pubspec_to_json.dart (pubspec_parse now has toJson) - Fix unused_catch_stack in dependency_validator.dart Co-authored-by: Dustin Pauze --- lib/src/constants.dart | 4 +- lib/src/dependency_validator.dart | 29 ++- lib/src/pubspec_config.dart | 2 +- test/executable_test.dart | 36 ++-- test/import_export_ast_visitor_test.dart | 80 ++------ test/pubspec_to_json.dart | 56 ----- test/utils.dart | 6 +- test/utils_test.dart | 5 +- test/workspace_test.dart | 250 +++++++++++------------ 9 files changed, 182 insertions(+), 286 deletions(-) delete mode 100644 test/pubspec_to_json.dart diff --git a/lib/src/constants.dart b/lib/src/constants.dart index a7e7d29..17274a6 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -53,8 +53,8 @@ class DependencyPinEvaluation { /// possible prerelease. static const DependencyPinEvaluation buildOrPrerelease = DependencyPinEvaluation._( - 'Builds or preleases as max bounds block minor bumps and patches.', - ); + 'Builds or preleases as max bounds block minor bumps and patches.', + ); /// 1.2.3 static const DependencyPinEvaluation directPin = DependencyPinEvaluation._( diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 93d61e0..bb83e0e 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -58,7 +58,7 @@ Future checkPackage({required String root}) async { .map((s) { try { return makeGlob("$root/$s"); - } catch (_, __) { + } catch (_) { logger.shout(yellow.wrap('invalid glob syntax: "$s"')); return null; } @@ -278,9 +278,9 @@ Future checkPackage({required String root}) async { 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)) + .difference(packagesUsedInPublicFiles) + // Intersect with deps that are used outside lib/ (excludes unused deps) + .intersection(packagesUsedOutsidePublicDirs)) // Doc imports in lib/ are not runtime deps; accept either dependencies or // dev_dependencies without flagging over-promotion. ..removeAll(packagesUsedViaDocImportInPublicFiles) @@ -340,11 +340,12 @@ Future checkPackage({required String root}) async { pubspec.dependencies.keys, '.', ); - bool rootPackageReferencesDependencyInBuildYaml(String dependencyName) => [ - ...rootBuildConfig.globalOptions.keys, - for (final target in rootBuildConfig.buildTargets.values) - ...target.builders.keys, - ] + bool rootPackageReferencesDependencyInBuildYaml(String dependencyName) => + [ + ...rootBuildConfig.globalOptions.keys, + for (final target in rootBuildConfig.buildTargets.values) + ...target.builders.keys, + ] .map((key) => normalizeBuilderKeyUsage(key, pubspec.name)) .any((key) => key.startsWith('$dependencyName:')); @@ -383,8 +384,9 @@ Future checkPackage({required String root}) async { if (providesExecutable(package)) package, }; - final nonDevPackagesWithExecutables = - packagesWithExecutables.where(pubspec.dependencies.containsKey).toSet(); + final nonDevPackagesWithExecutables = packagesWithExecutables + .where(pubspec.dependencies.containsKey) + .toSet(); if (nonDevPackagesWithExecutables.isNotEmpty) { logIntersection( Level.WARNING, @@ -431,10 +433,7 @@ 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/pubspec_config.dart b/lib/src/pubspec_config.dart index c9400a4..470fb1c 100644 --- a/lib/src/pubspec_config.dart +++ b/lib/src/pubspec_config.dart @@ -17,7 +17,7 @@ class PubspecDepValidatorConfig { dependencyValidator.ignore.isNotEmpty; PubspecDepValidatorConfig({DepValidatorConfig? dependencyValidator}) - : dependencyValidator = dependencyValidator ?? DepValidatorConfig(); + : dependencyValidator = dependencyValidator ?? DepValidatorConfig(); factory PubspecDepValidatorConfig.fromJson(Map json) => _$PubspecDepValidatorConfigFromJson(json); diff --git a/test/executable_test.dart b/test/executable_test.dart index 6810bf7..d8f9096 100644 --- a/test/executable_test.dart +++ b/test/executable_test.dart @@ -270,16 +270,13 @@ void main() { environment: requireDart38, project: [ d.dir('lib', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]), ], ); @@ -296,16 +293,13 @@ class Foo {} environment: requireDart38, project: [ d.dir('lib', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]), ], ); @@ -329,16 +323,13 @@ class Foo {} environment: requireDart38, project: [ d.dir('lib', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]), ], ); @@ -354,16 +345,13 @@ class Foo {} environment: requireDart38, project: [ d.dir('lib', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]), ], ); @@ -371,9 +359,11 @@ class Foo {} expect(result.exitCode, 0); expect( result.stderr, - isNot(contains( - 'These packages may be unused, or you may be using assets from these packages:', - )), + isNot( + contains( + 'These packages may be unused, or you may be using assets from these packages:', + ), + ), ); }); }); diff --git a/test/import_export_ast_visitor_test.dart b/test/import_export_ast_visitor_test.dart index fbfd47f..5870355 100644 --- a/test/import_export_ast_visitor_test.dart +++ b/test/import_export_ast_visitor_test.dart @@ -8,18 +8,13 @@ void main() { group('getDartPackageUsage', () { test('collects import and export directives', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' import 'package:logging/logging.dart'; export 'package:meta/meta.dart'; -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.directivePackageNames, {'logging', 'meta'}); expect(usage.docImportPackageNames, isEmpty); @@ -27,21 +22,16 @@ export 'package:meta/meta.dart'; test('collects doc imports from documentation comments', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; library; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.directivePackageNames, isEmpty); expect(usage.docImportPackageNames, {'meta'}); @@ -51,15 +41,12 @@ class Foo {} 'collects doc imports from declaration doc comments without a library directive', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; /// References [Deprecated]. class Foo {} -''', - ), +'''), ]).create(); final usage = getDartPackageUsage( @@ -73,9 +60,7 @@ class Foo {} test('collects both directives and doc imports', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:yaml/yaml.dart'; library; @@ -83,13 +68,10 @@ import 'package:logging/logging.dart'; /// References [YamlMap]. class Foo {} -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.directivePackageNames, {'logging'}); expect(usage.docImportPackageNames, {'yaml'}); @@ -97,40 +79,30 @@ class Foo {} test('collects package names from doc imports with show clauses', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:collection/collection.dart' show IterableExtension; /// References [IterableExtension]. class Foo {} -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.docImportPackageNames, {'collection'}); }); test('collects package names from doc imports with as clauses', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:collection/collection.dart' as collection; /// References [collection.IterableExtension]. class Foo {} -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.docImportPackageNames, {'collection'}); }); @@ -138,15 +110,12 @@ class Foo {} test('collects doc imports from bin/ files', () async { await d.dir('project', [ d.dir('bin', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; /// References [Deprecated]. void main() {} -''', - ), +'''), ]), ]).create(); @@ -160,18 +129,13 @@ void main() {} test('ignores relative and dart scheme imports', () async { await d.dir('project', [ - d.file( - 'main.dart', - ''' + d.file('main.dart', ''' /// @docImport 'dart:async'; import 'other.dart'; -''', - ), +'''), ]).create(); - final usage = getDartPackageUsage( - File('${d.sandbox}/project/main.dart'), - ); + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); expect(usage.directivePackageNames, isEmpty); expect(usage.docImportPackageNames, isEmpty); diff --git a/test/pubspec_to_json.dart b/test/pubspec_to_json.dart deleted file mode 100644 index cadcbf6..0000000 --- a/test/pubspec_to_json.dart +++ /dev/null @@ -1,56 +0,0 @@ -import "package:pubspec_parse/pubspec_parse.dart"; - -extension on Map { - Iterable<(K, V)> get records sync* { - for (final entry in entries) { - yield (entry.key, entry.value); - } - } -} - -typedef Json = Map; - -extension on Dependency { - Json toJson() => switch (this) { - SdkDependency(:final sdk, :final version) => { - "sdk": sdk, - "version": version.toString(), - }, - HostedDependency(:final hosted, :final version) => { - if (hosted != null) "hosted": hosted.url.toString(), - "version": version.toString(), - }, - GitDependency(:final url, :final ref, :final path) => { - "git": { - "url": url.toString(), - if (path != null) "ref": ref, - if (path != null) "path": path, - }, - }, - PathDependency(:final path) => {"path": path.replaceAll(r'\', '/')}, - }; -} - -/// An as-needed implementation of `Pubspec.toJson` for testing. -/// -/// See: https://github.com/dart-lang/tools/issues/1801 -extension PubspecToJson on Pubspec { - Json toJson() => { - "name": name, - "environment": { - for (final (sdk, version) in environment.records) - sdk: version.toString(), - }, - if (resolution != null) "resolution": resolution, - if (workspace != null) "workspace": workspace, - "dependencies": { - for (final (name, dependency) in dependencies.records) - name: dependency.toJson(), - }, - "dev_dependencies": { - for (final (name, dependency) in devDependencies.records) - name: dependency.toJson(), - }, - // ... - }; -} diff --git a/test/utils.dart b/test/utils.dart index febd158..8779799 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -11,8 +11,6 @@ import 'package:test_descriptor/test_descriptor.dart' as d; export 'package:logging/logging.dart' show Level; -import 'pubspec_to_json.dart'; - Future checkProject({ DepValidatorConfig? config, Map dependencies = const {}, @@ -48,8 +46,8 @@ Future checkProject({ } Dependency hostedCompatibleWith(String version) => HostedDependency( - version: VersionConstraint.compatibleWith(Version.parse(version)), - ); + version: VersionConstraint.compatibleWith(Version.parse(version)), +); Dependency hostedPinned(String version) => HostedDependency(version: Version.parse(version)); diff --git a/test/utils_test.dart b/test/utils_test.dart index 4c7407c..d6cdef1 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -134,8 +134,9 @@ include: package:pedantic/analysis_options.1.8.0.yaml expect(input, matches(importExportDartPackageRegex)); - final allMatches = - importExportDartPackageRegex.allMatches(input).toList(); + final allMatches = importExportDartPackageRegex + .allMatches(input) + .toList(); expect(allMatches, hasLength(2)); expect(allMatches[0].groups([1, 2]), [importOrExport, 'foo']); diff --git a/test/workspace_test.dart b/test/workspace_test.dart index 8498d39..223e245 100644 --- a/test/workspace_test.dart +++ b/test/workspace_test.dart @@ -25,128 +25,128 @@ final dependsOnMeta = { final excludeMain = DepValidatorConfig(exclude: ['lib/main.dart']); void main() => group('Workspaces', () { - initLogs(); - test( - 'works in the trivial case', - () => checkWorkspace( - workspaceDeps: {}, - workspace: [], - subpackage: [], - subpackageDeps: {}, - ), - ); - - test( - 'works in a basic case', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: dependsOnHttp, - subpackage: usesHttp, - subpackageDeps: dependsOnHttp, - ), - ); - - test( - 'works when the packages have different dependencies', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: dependsOnHttp, - subpackage: usesMeta, - subpackageDeps: dependsOnMeta, - ), - ); - - group('fails when the root has an issue', () { - test( - '(sub-package is okay)', - () => checkWorkspace( - workspace: [], - workspaceDeps: {}, - subpackage: usesHttp, - subpackageDeps: dependsOnHttp, - ), - ); - - test( - 'even when it shares a dependency with the subpackage', - () => checkWorkspace( - workspaceDeps: dependsOnHttp, - workspace: [], - subpackageDeps: dependsOnHttp, - subpackage: usesHttp, - matcher: isFalse, - ), - ); - }); - - group('fails when the subpackage has an issue', () { - test( - '(root is okay)', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: dependsOnHttp, - subpackage: [], - subpackageDeps: {}, - ), - ); - - test( - 'even when it shares a dependency with the subpackage', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: dependsOnHttp, - subpackage: usesHttp, - subpackageDeps: {}, - matcher: isFalse, - ), - ); - }); - - group('handles configs', () { - test( - 'at the root', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: {}, - workspaceConfig: excludeMain, - subpackage: [], - subpackageDeps: {}, - ), - ); - - test( - 'and fails at root when config is in subpackage', - () => checkWorkspace( - workspace: usesHttp, - workspaceDeps: {}, - subpackage: [], - subpackageDeps: {}, - subpackageConfig: excludeMain, - matcher: isFalse, - ), - ); - - test( - 'in a subpackage', - () => checkWorkspace( - workspace: [], - workspaceDeps: {}, - subpackage: usesHttp, - subpackageDeps: {}, - subpackageConfig: excludeMain, - ), - ); - - test( - 'and fails in subpackage when config is in root', - () => checkWorkspace( - workspace: [], - workspaceDeps: {}, - workspaceConfig: excludeMain, - subpackage: usesHttp, - subpackageDeps: {}, - matcher: isFalse, - ), - ); - }); - }); + initLogs(); + test( + 'works in the trivial case', + () => checkWorkspace( + workspaceDeps: {}, + workspace: [], + subpackage: [], + subpackageDeps: {}, + ), + ); + + test( + 'works in a basic case', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesHttp, + subpackageDeps: dependsOnHttp, + ), + ); + + test( + 'works when the packages have different dependencies', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesMeta, + subpackageDeps: dependsOnMeta, + ), + ); + + group('fails when the root has an issue', () { + test( + '(sub-package is okay)', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + subpackage: usesHttp, + subpackageDeps: dependsOnHttp, + ), + ); + + test( + 'even when it shares a dependency with the subpackage', + () => checkWorkspace( + workspaceDeps: dependsOnHttp, + workspace: [], + subpackageDeps: dependsOnHttp, + subpackage: usesHttp, + matcher: isFalse, + ), + ); + }); + + group('fails when the subpackage has an issue', () { + test( + '(root is okay)', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: [], + subpackageDeps: {}, + ), + ); + + test( + 'even when it shares a dependency with the subpackage', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesHttp, + subpackageDeps: {}, + matcher: isFalse, + ), + ); + }); + + group('handles configs', () { + test( + 'at the root', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: {}, + workspaceConfig: excludeMain, + subpackage: [], + subpackageDeps: {}, + ), + ); + + test( + 'and fails at root when config is in subpackage', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: {}, + subpackage: [], + subpackageDeps: {}, + subpackageConfig: excludeMain, + matcher: isFalse, + ), + ); + + test( + 'in a subpackage', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + subpackage: usesHttp, + subpackageDeps: {}, + subpackageConfig: excludeMain, + ), + ); + + test( + 'and fails in subpackage when config is in root', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + workspaceConfig: excludeMain, + subpackage: usesHttp, + subpackageDeps: {}, + matcher: isFalse, + ), + ); + }); +}); From 556d3c0af69210163a52c9ec1b6cfef674f2425d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 19:45:42 +0000 Subject: [PATCH 4/8] FEDX-7267: address remaining PR review feedback for doc imports - Remove hardcoded FeatureSet from parseString (use analyzer default) - Bump version to 6.0.0 to reflect breaking SDK floor (^3.8.0) - Fix CHANGELOG wording for doc-import dependency requirements - Bump pubspec_parse to ^1.6.0 for native Pubspec.toJson() in tests Co-authored-by: Dustin Pauze --- CHANGELOG.md | 9 +++++---- lib/src/import_export_ast_visitor.dart | 11 +---------- pubspec.yaml | 4 ++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cb239..79d29f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Unreleased -# 5.1.0 +# 6.0.0 - Added support for Dart `@docImport` documentation imports when scanning package usage. -- Packages referenced only via `@docImport` in `lib/` are accepted as either - `dependencies` or `dev_dependencies` and are not flagged as over-promoted, - missing, or unused. +- Packages referenced only via `@docImport` in `lib/` must still be declared + in `pubspec.yaml` as either a `dependency` or `dev_dependency`; when + declared, they are accepted in either section and are not flagged as + over-promoted or unused. - Removed the internal `getDartDirectivePackageNames` API in favor of `getDartPackageUsage`. - Requires Dart 3.8 or above. diff --git a/lib/src/import_export_ast_visitor.dart b/lib/src/import_export_ast_visitor.dart index 0de8d20..a1280b8 100644 --- a/lib/src/import_export_ast_visitor.dart +++ b/lib/src/import_export_ast_visitor.dart @@ -1,11 +1,9 @@ import 'dart:io'; -import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:pub_semver/pub_semver.dart'; /// Package names referenced in a Dart file via import/export directives and /// doc imports. @@ -26,14 +24,7 @@ class DartPackageUsage { DartPackageUsage getDartPackageUsage(File file) { ParseStringResult parsed; try { - parsed = parseString( - content: file.readAsStringSync(), - path: file.path, - featureSet: FeatureSet.fromEnableFlags2( - sdkLanguageVersion: Version.parse('3.8.0'), - flags: const [], - ), - ); + parsed = parseString(content: file.readAsStringSync(), path: file.path); } on ArgumentError catch (e) { print('Error parsing: ${file.path}'); print(e.message); diff --git a/pubspec.yaml b/pubspec.yaml index 65953e7..9ac67a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dependency_validator -version: 5.1.0 +version: 6.0.0 description: Checks for missing, under-promoted, over-promoted, and unused dependencies. homepage: https://github.com/Workiva/dependency_validator @@ -18,7 +18,7 @@ dependencies: package_config: ">=2.0.0 <4.0.0" path: ^1.8.0 pub_semver: ^2.0.0 - pubspec_parse: ^1.5.0 + pubspec_parse: ^1.6.0 yaml: ^3.1.0 dev_dependencies: From f67c87f2f0d205c1fc942ead96040599652cf8f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 20:22:29 +0000 Subject: [PATCH 5/8] FEDX-7267: address remaining PR review feedback for doc imports - Restore test/pubspec_to_json.dart with pubspecToJson() that omits null entries - Scan beginToken.precedingComments for dangling file-level @docImport comments - Fix doc-import AST visitor tests and add regression for dangling comments - Raise analyzer lower bound to >=8.0.0 for Comment.docImports support - Remove empty CHANGELOG Unreleased heading Co-authored-by: Dustin Pauze --- CHANGELOG.md | 3 -- lib/src/import_export_ast_visitor.dart | 53 ++++++++++++++++++------ pubspec.yaml | 2 +- test/import_export_ast_visitor_test.dart | 22 +++++++++- test/pubspec_to_json.dart | 27 ++++++++++++ test/utils.dart | 8 ++-- 6 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 test/pubspec_to_json.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d29f4..ae05e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,3 @@ -# Unreleased - - # 6.0.0 - Added support for Dart `@docImport` documentation imports when scanning diff --git a/lib/src/import_export_ast_visitor.dart b/lib/src/import_export_ast_visitor.dart index a1280b8..827cc86 100644 --- a/lib/src/import_export_ast_visitor.dart +++ b/lib/src/import_export_ast_visitor.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; /// Package names referenced in a Dart file via import/export directives and @@ -33,12 +34,52 @@ DartPackageUsage getDartPackageUsage(File file) { final visitor = ImportExportVisitor(); parsed.unit.accept(visitor); + _collectDocImportsFromPrecedingComments( + parsed.unit.beginToken.precedingComments, + visitor.docImportPackageNames, + ); return DartPackageUsage( directivePackageNames: visitor.directivePackageNames, docImportPackageNames: visitor.docImportPackageNames, ); } +void _collectDocImportsFromPrecedingComments( + Token? commentToken, + Set docImportPackageNames, +) { + for (var token = commentToken; token != null; token = token.next) { + if (token is! CommentToken) continue; + _collectDocImportsFromCommentLexeme(token.lexeme, docImportPackageNames); + } +} + +final _docImportUriPattern = RegExp( + r'''@docImport\s+(['"])(.+?)\1''', + multiLine: true, +); + +void _collectDocImportsFromCommentLexeme( + String lexeme, + Set docImportPackageNames, +) { + for (final match in _docImportUriPattern.allMatches(lexeme)) { + _addPackageName(match.group(2), docImportPackageNames); + } +} + +void _addPackageName(String? uri, Set packageNames) { + if (uri == null) return; + + // ignore relative path imports + if (!uri.startsWith('package:')) return; + + final packageParts = uri.substring('package:'.length).split('/'); + if (packageParts.isEmpty) return; + + packageNames.add(packageParts.first); +} + class ImportExportVisitor extends GeneralizingAstVisitor { Set directivePackageNames = {}; Set docImportPackageNames = {}; @@ -58,16 +99,4 @@ class ImportExportVisitor extends GeneralizingAstVisitor { } super.visitComment(node); } - - void _addPackageName(String? uri, Set packageNames) { - if (uri == null) return; - - // ignore relative path imports - if (!uri.startsWith('package:')) return; - - final packageParts = uri.substring('package:'.length).split('/'); - if (packageParts.isEmpty) return; - - packageNames.add(packageParts.first); - } } diff --git a/pubspec.yaml b/pubspec.yaml index 9ac67a6..ee3d67c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: ^3.8.0 dependencies: - analyzer: ">=7.1.0 <15.0.0" + analyzer: ">=8.0.0 <15.0.0" args: ^2.0.0 build_config: ^1.0.0 checked_yaml: ^2.0.1 diff --git a/test/import_export_ast_visitor_test.dart b/test/import_export_ast_visitor_test.dart index 5870355..9a2aa3e 100644 --- a/test/import_export_ast_visitor_test.dart +++ b/test/import_export_ast_visitor_test.dart @@ -43,7 +43,6 @@ class Foo {} await d.dir('project', [ d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; - /// References [Deprecated]. class Foo {} '''), @@ -58,6 +57,24 @@ class Foo {} }, ); + test( + 'collects file-level dangling doc imports from beginToken.precedingComments', + () async { + await d.dir('project', [ + d.file('main.dart', ''' +/// @docImport 'package:meta/meta.dart'; +'''), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, {'meta'}); + }, + ); + test('collects both directives and doc imports', () async { await d.dir('project', [ d.file('main.dart', ''' @@ -81,6 +98,7 @@ class Foo {} await d.dir('project', [ d.file('main.dart', ''' /// @docImport 'package:collection/collection.dart' show IterableExtension; +library; /// References [IterableExtension]. class Foo {} @@ -96,6 +114,7 @@ class Foo {} await d.dir('project', [ d.file('main.dart', ''' /// @docImport 'package:collection/collection.dart' as collection; +library; /// References [collection.IterableExtension]. class Foo {} @@ -112,7 +131,6 @@ class Foo {} d.dir('bin', [ d.file('main.dart', ''' /// @docImport 'package:meta/meta.dart'; - /// References [Deprecated]. void main() {} '''), diff --git a/test/pubspec_to_json.dart b/test/pubspec_to_json.dart new file mode 100644 index 0000000..9b4b381 --- /dev/null +++ b/test/pubspec_to_json.dart @@ -0,0 +1,27 @@ +import 'package:pubspec_parse/pubspec_parse.dart'; + +typedef Json = Map; + +/// Serializes [pubspec] for test sandbox `pubspec.yaml` files. +/// +/// [Pubspec.toJson] from `pubspec_parse` includes null fields that `pub` +/// rejects, so this helper omits null and empty entries. +Json pubspecToJson(Pubspec pubspec) => _omitNullAndEmpty(pubspec.toJson()); + +Json _omitNullAndEmpty(Json json) { + final result = {}; + for (final entry in json.entries) { + final value = entry.value; + if (value == null) continue; + if (value is Map) { + final nested = _omitNullAndEmpty(Map.from(value)); + if (nested.isNotEmpty) { + result[entry.key] = nested; + } + continue; + } + if (value is List && value.isEmpty) continue; + result[entry.key] = value; + } + return result; +} diff --git a/test/utils.dart b/test/utils.dart index 8779799..566b93f 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -9,6 +9,8 @@ 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'; + export 'package:logging/logging.dart' show Level; Future checkProject({ @@ -29,7 +31,7 @@ Future checkProject({ 'dependency_validator': PathDependency(Directory.current.absolute.path), }, ); - final pubspecJson = pubspec.toJson(); + final pubspecJson = pubspecToJson(pubspec); if (embedConfigInPubspec && config != null) { pubspecJson['dependency_validator'] = config.toJson(); } @@ -96,7 +98,7 @@ Future checkWorkspace({ ); final dir = d.dir('workspace', [ ...workspace, - d.file('pubspec.yaml', jsonEncode(workspacePubspec.toJson())), + d.file('pubspec.yaml', jsonEncode(pubspecToJson(workspacePubspec))), if (workspaceConfig != null) d.file( 'dart_dependency_validator.yaml', @@ -104,7 +106,7 @@ Future checkWorkspace({ ), d.dir('subpackage', [ ...subpackage, - d.file('pubspec.yaml', jsonEncode(subpackagePubspec.toJson())), + d.file('pubspec.yaml', jsonEncode(pubspecToJson(subpackagePubspec))), if (subpackageConfig != null) d.file( 'dart_dependency_validator.yaml', From d019a32232b87f6da918c393d1cae2f8513ed01b Mon Sep 17 00:00:00 2001 From: Dustin Pauze Date: Wed, 9 Sep 2026 09:13:27 -0400 Subject: [PATCH 6/8] FEDX-7267: fix doc import false positives and narrow over-promotion exemption --- CHANGELOG.md | 8 +-- lib/src/dependency_validator.dart | 30 +++++++---- lib/src/import_export_ast_visitor.dart | 56 ++++++++++++++----- pubspec.yaml | 2 +- test/executable_test.dart | 68 ++++++++++++++++++++++++ test/import_export_ast_visitor_test.dart | 57 ++++++++++++++++++++ 6 files changed, 196 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae05e6a..09e94cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,16 @@ -# 6.0.0 +# Unreleased + - Added support for Dart `@docImport` documentation imports when scanning package usage. - Packages referenced only via `@docImport` in `lib/` must still be declared in `pubspec.yaml` as either a `dependency` or `dev_dependency`; when declared, they are accepted in either section and are not flagged as - over-promoted or unused. + over-promoted or unused. If such a package also has real imports outside + `lib/`, the normal over-promotion check still applies. - Removed the internal `getDartDirectivePackageNames` API in favor of `getDartPackageUsage`. -- Requires Dart 3.8 or above. +- **Breaking:** requires Dart 3.8 or above. # 5.0.6 diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index bb83e0e..52ad998 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -197,21 +197,20 @@ Future checkPackage({required String root}) async { ); // Read each file outside lib/ and parse the package names from every - // import and export directive. + // import, export directive, and doc import. final packagesUsedOutsidePublicDirs = { // For more info on analysis options: // https://dart.dev/guides/language/analysis-options#the-analysis-options-file if (optionsIncludePackage != null) optionsIncludePackage, }; + final packagesUsedViaDocImportOutsidePublicDirs = {}; for (final file in nonPublicDartFiles) { final usage = getDartPackageUsage(file); packagesUsedOutsidePublicDirs.addAll(usage.directivePackageNames); - packagesUsedOutsidePublicDirs.addAll(usage.docImportPackageNames); + packagesUsedViaDocImportOutsidePublicDirs.addAll( + usage.docImportPackageNames, + ); } - - // Doc imports in lib/ are not runtime dependencies, so treat them like usage - // outside lib/ for dependency promotion checks. - packagesUsedOutsidePublicDirs.addAll(packagesUsedViaDocImportInPublicFiles); for (final file in nonPublicScssFiles) { final matches = importScssPackageRegex.allMatches(file.readAsStringSync()); for (final match in matches) { @@ -225,6 +224,19 @@ Future checkPackage({required String root}) async { } } + // Packages that are doc-imported in lib/ and have no real (non-doc) usage + // anywhere outside lib/. Doc imports are not runtime dependencies, so these + // are valid in either `dependencies` or `dev_dependencies`. A package with + // real usage outside lib/ is still subject to the normal over-promotion check. + final packagesUsedOnlyViaDocImport = packagesUsedViaDocImportInPublicFiles + .difference(packagesUsedOutsidePublicDirs); + + // Doc imports are not runtime dependencies, so treat them like usage outside + // lib/ for the missing/unused dependency checks. + packagesUsedOutsidePublicDirs + ..addAll(packagesUsedViaDocImportOutsidePublicDirs) + ..addAll(packagesUsedViaDocImportInPublicFiles); + logger.fine( 'packages used outside public dirs:\n' '${bulletItems(packagesUsedOutsidePublicDirs)}\n', @@ -281,9 +293,9 @@ Future checkPackage({required String root}) async { .difference(packagesUsedInPublicFiles) // Intersect with deps that are used outside lib/ (excludes unused deps) .intersection(packagesUsedOutsidePublicDirs)) - // Doc imports in lib/ are not runtime deps; accept either dependencies or - // dev_dependencies without flagging over-promotion. - ..removeAll(packagesUsedViaDocImportInPublicFiles) + // Doc-import-only packages are accepted in either dependencies or + // dev_dependencies. + ..removeAll(packagesUsedOnlyViaDocImport) // Ignore known over-promoted packages. ..removeAll(ignoredPackages); diff --git a/lib/src/import_export_ast_visitor.dart b/lib/src/import_export_ast_visitor.dart index 827cc86..bdcfabe 100644 --- a/lib/src/import_export_ast_visitor.dart +++ b/lib/src/import_export_ast_visitor.dart @@ -44,28 +44,60 @@ DartPackageUsage getDartPackageUsage(File file) { ); } +/// Collects `@docImport` package names from comment tokens that are not +/// attached to any AST node (e.g. a file containing only a doc comment). +/// +/// Mirrors the analyzer's own doc comment parsing as closely as is practical: +/// only `///` and `/** */` doc comments are considered, `@docImport` must start +/// a line, and fenced code blocks are skipped. void _collectDocImportsFromPrecedingComments( Token? commentToken, Set docImportPackageNames, ) { + var inFencedCodeBlock = false; for (var token = commentToken; token != null; token = token.next) { if (token is! CommentToken) continue; - _collectDocImportsFromCommentLexeme(token.lexeme, docImportPackageNames); + + final lexeme = token.lexeme; + final isBlockDocComment = lexeme.startsWith('/**'); + if (!isBlockDocComment && !lexeme.startsWith('///')) continue; + + // A block doc comment is self-contained; don't carry fence state into it. + if (isBlockDocComment) inFencedCodeBlock = false; + + for (final line in lexeme.split('\n')) { + final content = _stripDocCommentDecoration(line); + if (content.startsWith('```')) { + inFencedCodeBlock = !inFencedCodeBlock; + continue; + } + if (inFencedCodeBlock) continue; + _collectDocImportFromLine(content, docImportPackageNames); + } } } -final _docImportUriPattern = RegExp( - r'''@docImport\s+(['"])(.+?)\1''', - multiLine: true, -); - -void _collectDocImportsFromCommentLexeme( - String lexeme, - Set docImportPackageNames, -) { - for (final match in _docImportUriPattern.allMatches(lexeme)) { - _addPackageName(match.group(2), docImportPackageNames); +/// Strips the leading `///`, `/**`, or ` * ` and trailing `*/` from a single +/// line of a doc comment lexeme. +String _stripDocCommentDecoration(String line) { + var content = line.trim(); + if (content.startsWith('///') || content.startsWith('/**')) { + content = content.substring(3); + } else if (content.startsWith('*')) { + content = content.substring(1); } + if (content.endsWith('*/')) { + content = content.substring(0, content.length - 2); + } + return content.trim(); +} + +final _docImportUriPattern = RegExp(r'''^@docImport\s+(['"])(.+?)\1'''); + +void _collectDocImportFromLine(String line, Set docImportPackageNames) { + final match = _docImportUriPattern.firstMatch(line); + if (match == null) return; + _addPackageName(match.group(2), docImportPackageNames); } void _addPackageName(String? uri, Set packageNames) { diff --git a/pubspec.yaml b/pubspec.yaml index ee3d67c..d65e9eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dependency_validator -version: 6.0.0 +version: 5.0.6 description: Checks for missing, under-promoted, over-promoted, and unused dependencies. homepage: https://github.com/Workiva/dependency_validator diff --git a/test/executable_test.dart b/test/executable_test.dart index d8f9096..20c4570 100644 --- a/test/executable_test.dart +++ b/test/executable_test.dart @@ -339,6 +339,74 @@ class Foo {} }, ); + test('flags a dependency as over-promoted when it is doc-imported in lib/ ' + 'but only truly imported outside lib/', () async { + result = await checkProject( + dependencies: {'meta': hostedAny}, + environment: requireDart38, + project: [ + d.dir('lib', [ + d.file('main.dart', ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +'''), + ]), + d.dir('test', [ + d.file('main_test.dart', ''' +import 'package:meta/meta.dart'; + +void main() {} +'''), + ]), + ], + ); + + expect(result.exitCode, 1); + expect( + result.stderr, + contains( + 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', + ), + ); + expect(result.stderr, contains('meta')); + }); + + test( + 'accepts a dependency that is doc-imported in both lib/ and outside lib/', + () async { + result = await checkProject( + dependencies: {'meta': hostedAny}, + environment: requireDart38, + project: [ + d.dir('lib', [ + d.file('main.dart', ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +class Foo {} +'''), + ]), + d.dir('test', [ + d.file('main_test.dart', ''' +/// @docImport 'package:meta/meta.dart'; +library; + +/// References [Deprecated]. +void main() {} +'''), + ]), + ], + ); + + expect(result.exitCode, 0); + expect(result.stdout, contains('No dependency issues found!')); + }, + ); + test('does not flag doc-import-only packages as unused', () async { result = await checkProject( devDependencies: {'meta': hostedAny}, diff --git a/test/import_export_ast_visitor_test.dart b/test/import_export_ast_visitor_test.dart index 9a2aa3e..7653822 100644 --- a/test/import_export_ast_visitor_test.dart +++ b/test/import_export_ast_visitor_test.dart @@ -75,6 +75,63 @@ class Foo {} }, ); + test('ignores @docImport text in non-doc comments', () async { + await d.dir('project', [ + d.file('main.dart', ''' +// @docImport 'package:meta/meta.dart'; +/* @docImport 'package:yaml/yaml.dart'; */ +// /// @docImport 'package:logging/logging.dart'; +class Foo {} +'''), + ]).create(); + + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); + + expect(usage.directivePackageNames, isEmpty); + expect(usage.docImportPackageNames, isEmpty); + }); + + test( + 'ignores @docImport inside fenced code blocks in doc comments', + () async { + await d.dir('project', [ + d.file('main.dart', ''' +/// Example: +/// ```dart +/// /// @docImport 'package:meta/meta.dart'; +/// ``` +library; + +/** Another example: + * ``` + * /// @docImport 'package:yaml/yaml.dart'; + * ``` + */ +class Foo {} +'''), + ]).create(); + + final usage = getDartPackageUsage( + File('${d.sandbox}/project/main.dart'), + ); + + expect(usage.docImportPackageNames, isEmpty); + }, + ); + + test('ignores @docImport mentioned mid-line in a doc comment', () async { + await d.dir('project', [ + d.file('main.dart', ''' +/// Use `@docImport 'package:meta/meta.dart';` to reference [Deprecated]. +library; +'''), + ]).create(); + + final usage = getDartPackageUsage(File('${d.sandbox}/project/main.dart')); + + expect(usage.docImportPackageNames, isEmpty); + }); + test('collects both directives and doc imports', () async { await d.dir('project', [ d.file('main.dart', ''' From c8374fb80b4a809d42ef2fe0e412bd6153203747 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 18:42:58 +0000 Subject: [PATCH 7/8] FEDX-7267: fix format and analyze CI checks Co-authored-by: Dustin Pauze --- lib/src/pubspec_config.g.dart | 23 +-- test/nested_packages_test.dart | 360 ++++++++++++++++----------------- test/utils_test.dart | 15 +- 3 files changed, 194 insertions(+), 204 deletions(-) diff --git a/lib/src/pubspec_config.g.dart b/lib/src/pubspec_config.g.dart index 5d9586b..d9c8e4c 100644 --- a/lib/src/pubspec_config.g.dart +++ b/lib/src/pubspec_config.g.dart @@ -7,20 +7,15 @@ part of 'pubspec_config.dart'; // ************************************************************************** PubspecDepValidatorConfig _$PubspecDepValidatorConfigFromJson(Map json) => - $checkedCreate( - 'PubspecDepValidatorConfig', - json, - ($checkedConvert) { - final val = PubspecDepValidatorConfig( - dependencyValidator: $checkedConvert( - 'dependency_validator', - (v) => v == null ? null : DepValidatorConfig.fromJson(v as Map), - ), - ); - return val; - }, - fieldKeyMap: const {'dependencyValidator': 'dependency_validator'}, - ); + $checkedCreate('PubspecDepValidatorConfig', json, ($checkedConvert) { + final val = PubspecDepValidatorConfig( + 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) { diff --git a/test/nested_packages_test.dart b/test/nested_packages_test.dart index df1c643..15f9eb6 100644 --- a/test/nested_packages_test.dart +++ b/test/nested_packages_test.dart @@ -9,198 +9,192 @@ 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())), + 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(pubspecToJson(rootPubspec))), + 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(pubspecToJson(nestedPubspec))), + d.dir('tool', [ + d.file('ffigen.dart', 'import "package:ffigen/ffigen.dart";'), + ]), d.dir('lib', [ - d.file('code_assets.dart', 'import "package:http/http.dart";'), + d.file('host_name.dart', 'import "package:archive/archive.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(pubspecToJson(rootPubspec))), + 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(pubspecToJson(nestedPubspec))), + d.dir('tool', [ + d.file('ffigen.dart', 'import "package:ffigen/ffigen.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(pubspecToJson(rootPubspec))), + d.dir('example', [ + d.dir('nested', [ + d.dir('deep', [ + d.file( + 'pubspec.yaml', + jsonEncode(pubspecToJson(deeplyNestedPubspec)), + ), + d.dir('lib', [ + d.file('deep.dart', 'import "package:meta/meta.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(pubspecToJson(rootPubspec))), + d.dir('example', [ + d.dir('nested_web', [ + d.file('pubspec.yaml', jsonEncode(pubspecToJson(nestedPubspec))), + 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}/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(pubspecToJson(workspacePubspec))), + d.dir('pkgs', [ + d.dir('code_assets', [ + d.file( + 'pubspec.yaml', + jsonEncode(pubspecToJson(subpackagePubspec)), + ), + d.dir('lib', [ + d.file('code_assets.dart', 'import "package:http/http.dart";'), ]), - ]), - ]); - - 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";'), - ]), + d.dir('example', [ + d.dir('host_name', [ + d.file( + 'pubspec.yaml', + jsonEncode(pubspecToJson(nestedPubspec)), + ), + 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); - }); - }); + ]), + ]); + + await dir.create(); + final result = await checkPackage(root: '${d.sandbox}/workspace'); + expect(result, isTrue); + }, + ); +}); diff --git a/test/utils_test.dart b/test/utils_test.dart index b255a92..e50fc58 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -483,16 +483,17 @@ include: package:pedantic/analysis_options.1.8.0.yaml ]), ]), d.dir('.dart_tool', [ - d.dir('hidden_sub', [ - d.file('pubspec.yaml', 'name: hidden_sub'), - ]), + d.dir('hidden_sub', [d.file('pubspec.yaml', 'name: hidden_sub')]), ]), ]).create(); - final nested = listNestedPackages('${d.sandbox}/complex_pkg') - .map((dir) => p.relative(dir.path, from: '${d.sandbox}/complex_pkg')) - .toList() - ..sort(); + final nested = + listNestedPackages('${d.sandbox}/complex_pkg') + .map( + (dir) => p.relative(dir.path, from: '${d.sandbox}/complex_pkg'), + ) + .toList() + ..sort(); expect(nested, [ p.join('example', 'host_name'), From 45ec07aca0929e189b25f98b3762fd872f7963aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Sep 2026 11:59:09 +0000 Subject: [PATCH 8/8] FEDX-7267: format pubspec_config.g.dart with Dart 3.8.1 Co-authored-by: Dustin Pauze --- lib/src/pubspec_config.g.dart | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/src/pubspec_config.g.dart b/lib/src/pubspec_config.g.dart index d9c8e4c..5d9586b 100644 --- a/lib/src/pubspec_config.g.dart +++ b/lib/src/pubspec_config.g.dart @@ -7,15 +7,20 @@ part of 'pubspec_config.dart'; // ************************************************************************** PubspecDepValidatorConfig _$PubspecDepValidatorConfigFromJson(Map json) => - $checkedCreate('PubspecDepValidatorConfig', json, ($checkedConvert) { - final val = PubspecDepValidatorConfig( - dependencyValidator: $checkedConvert( - 'dependency_validator', - (v) => v == null ? null : DepValidatorConfig.fromJson(v as Map), - ), - ); - return val; - }, fieldKeyMap: const {'dependencyValidator': 'dependency_validator'}); + $checkedCreate( + 'PubspecDepValidatorConfig', + json, + ($checkedConvert) { + final val = PubspecDepValidatorConfig( + 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) {