Version 2.14.0-228.0.dev

Merge commit 'b929c663931d75c5b5c0520665133fd5d2f4470a' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
index 88fbe28..fce6916 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart
@@ -353,7 +353,19 @@
 
   @override
   OpType get opType {
-    return _opType ??= OpType.forCompletion(target, offset);
+    var opType = _opType;
+    if (opType == null) {
+      opType = OpType.forCompletion(target, offset);
+      var contextType = this.contextType;
+      if (contextType is FunctionType) {
+        contextType = contextType.returnType;
+      }
+      if (contextType != null && contextType.isVoid) {
+        opType.includeVoidReturnSuggestions = true;
+      }
+      _opType = opType;
+    }
+    return opType;
   }
 
   /// The source range that represents the region of text that should be
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/add_missing_enum_like_case_clauses.dart b/pkg/analysis_server/lib/src/services/correction/dart/add_missing_enum_like_case_clauses.dart
new file mode 100644
index 0000000..b3e308f
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/add_missing_enum_like_case_clauses.dart
@@ -0,0 +1,101 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+
+class AddMissingEnumLikeCaseClauses extends CorrectionProducer {
+  @override
+  FixKind get fixKind => DartFixKind.ADD_MISSING_ENUM_CASE_CLAUSES;
+
+  // TODO: Consider enabling this lint for fix all in file.
+  // @override
+  // FixKind? get multiFixKind => super.multiFixKind;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    final node = this.node;
+    if (node is SwitchStatement) {
+      var expressionType = node.expression.staticType;
+      if (expressionType is! InterfaceType) {
+        return;
+      }
+      var classElement = expressionType.element;
+      var className = classElement.name;
+      var caseNames = _caseNames(node);
+      var missingNames = _constantNames(classElement)
+        ..removeWhere((e) => caseNames.contains(e));
+      missingNames.sort();
+      var firstCase = node.members[0];
+      var caseIndent = utils.getLinePrefix(firstCase.offset);
+      var statementIndent = utils.getLinePrefix(firstCase.statements[0].offset);
+      // TODO(brianwilkerson) Consider inserting the names in order into the
+      //  switch statement.
+      await builder.addDartFileEdit(file, (builder) {
+        builder.addInsertion(node.members.last.end, (builder) {
+          for (var name in missingNames) {
+            builder.writeln();
+            builder.write(caseIndent);
+            builder.write('case ');
+            builder.write(className);
+            builder.write('.');
+            builder.write(name);
+            builder.writeln(':');
+            builder.write(statementIndent);
+            builder.writeln('// TODO: Handle this case.');
+            builder.write(statementIndent);
+            builder.write('break;');
+          }
+        });
+      });
+    }
+  }
+
+  /// Return the names of the constants already in a case clause in the
+  /// [statement].
+  List<String> _caseNames(SwitchStatement statement) {
+    var caseNames = <String>[];
+    for (var member in statement.members) {
+      if (member is SwitchCase) {
+        var expression = member.expression;
+        if (expression is Identifier) {
+          var element = expression.staticElement;
+          if (element is PropertyAccessorElement) {
+            caseNames.add(element.name);
+          }
+        } else if (expression is PropertyAccess) {
+          caseNames.add(expression.propertyName.name);
+        }
+      }
+    }
+    return caseNames;
+  }
+
+  /// Return the names of the constants defined in [classElement].
+  List<String> _constantNames(ClassElement classElement) {
+    var type = classElement.thisType;
+    var constantNames = <String>[];
+    for (var field in classElement.fields) {
+      // Ensure static const.
+      if (field.isSynthetic || !field.isConst || !field.isStatic) {
+        continue;
+      }
+      // Check for type equality.
+      if (field.type != type) {
+        continue;
+      }
+      constantNames.add(field.name);
+    }
+    return constantNames;
+  }
+
+  /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+  static AddMissingEnumLikeCaseClauses newInstance() =>
+      AddMissingEnumLikeCaseClauses();
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index 78b321a..61a67e1 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -16,6 +16,7 @@
 import 'package:analysis_server/src/services/correction/dart/add_field_formal_parameters.dart';
 import 'package:analysis_server/src/services/correction/dart/add_late.dart';
 import 'package:analysis_server/src/services/correction/dart/add_missing_enum_case_clauses.dart';
+import 'package:analysis_server/src/services/correction/dart/add_missing_enum_like_case_clauses.dart';
 import 'package:analysis_server/src/services/correction/dart/add_missing_parameter.dart';
 import 'package:analysis_server/src/services/correction/dart/add_missing_parameter_named.dart';
 import 'package:analysis_server/src/services/correction/dart/add_missing_required_argument.dart';
@@ -395,6 +396,9 @@
       RemoveEmptyStatement.newInstance,
       ReplaceWithBrackets.newInstance,
     ],
+    LintNames.exhaustive_cases: [
+      AddMissingEnumLikeCaseClauses.newInstance,
+    ],
     LintNames.hash_and_equals: [
       CreateMethod.equalsOrHashCode,
     ],
diff --git a/pkg/analysis_server/lib/src/services/linter/lint_names.dart b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
index 2f20834..20e6839 100644
--- a/pkg/analysis_server/lib/src/services/linter/lint_names.dart
+++ b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
@@ -44,6 +44,7 @@
   static const String empty_catches = 'empty_catches';
   static const String empty_constructor_bodies = 'empty_constructor_bodies';
   static const String empty_statements = 'empty_statements';
+  static const String exhaustive_cases = 'exhaustive_cases';
   static const String hash_and_equals = 'hash_and_equals';
   static const String no_duplicate_case_values = 'no_duplicate_case_values';
   static const String non_constant_identifier_names =
diff --git a/pkg/analysis_server/test/src/services/completion/dart/completion_test.dart b/pkg/analysis_server/test/src/services/completion/dart/completion_test.dart
index f8171dc..59352d0 100644
--- a/pkg/analysis_server/test/src/services/completion/dart/completion_test.dart
+++ b/pkg/analysis_server/test/src/services/completion/dart/completion_test.dart
@@ -20,6 +20,7 @@
     defineReflectiveTests(PropertyAccessCompletionTest);
     defineReflectiveTests(RedirectedConstructorCompletionTest);
     defineReflectiveTests(RedirectingConstructorInvocationCompletionTest);
+    defineReflectiveTests(ReturnStatementTest);
     defineReflectiveTests(SuperConstructorInvocationCompletionTest);
     defineReflectiveTests(VariableDeclarationListCompletionTest);
   });
@@ -585,6 +586,48 @@
 }
 
 @reflectiveTest
+class ReturnStatementTest extends CompletionTestCase {
+  Future<void> test_voidFromVoid_localFunction() async {
+    addTestFile('''
+class C {
+  void m() {
+    void f() {
+      return ^
+    }
+  }
+  void g() {}
+}
+''');
+    await getSuggestions();
+    assertHasCompletion('g');
+  }
+
+  Future<void> test_voidFromVoid_method() async {
+    addTestFile('''
+class C {
+  void f() {
+    return ^
+  }
+  void g() {}
+}
+''');
+    await getSuggestions();
+    assertHasCompletion('g');
+  }
+
+  Future<void> test_voidFromVoid_topLevelFunction() async {
+    addTestFile('''
+void f() {
+  return ^
+}
+void g() {}
+''');
+    await getSuggestions();
+    assertHasCompletion('g');
+  }
+}
+
+@reflectiveTest
 class SuperConstructorInvocationCompletionTest extends CompletionTestCase {
   Future<void> test_namedConstructor_notVisible() async {
     newFile('/project/bin/a.dart', content: '''
diff --git a/pkg/analysis_server/test/src/services/correction/fix/add_missing_enum_like_case_clauses_test.dart b/pkg/analysis_server/test/src/services/correction/fix/add_missing_enum_like_case_clauses_test.dart
new file mode 100644
index 0000000..6c299ca
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/add_missing_enum_like_case_clauses_test.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/linter/lint_names.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(AddMissingEnumLikeCaseClausesTest);
+  });
+}
+
+@reflectiveTest
+class AddMissingEnumLikeCaseClausesTest extends FixProcessorLintTest {
+  @override
+  FixKind get kind => DartFixKind.ADD_MISSING_ENUM_CASE_CLAUSES;
+
+  @override
+  String get lintCode => LintNames.exhaustive_cases;
+
+  Future<void> test_missing() async {
+    await resolveTestCode('''
+void f(E e) {
+  switch (e) {
+    case E.a:
+      break;
+    case E.b:
+      break;
+  }
+}
+class E {
+  static const E a = E._(0);
+  static const E b = E._(1);
+  static const E c = E._(2);
+  const E._(int x);
+}
+''');
+    await assertHasFix('''
+void f(E e) {
+  switch (e) {
+    case E.a:
+      break;
+    case E.b:
+      break;
+    case E.c:
+      // TODO: Handle this case.
+      break;
+  }
+}
+class E {
+  static const E a = E._(0);
+  static const E b = E._(1);
+  static const E c = E._(2);
+  const E._(int x);
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index f9946fa..ece1e25 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -15,6 +15,8 @@
 import 'add_late_test.dart' as add_late;
 import 'add_missing_enum_case_clauses_test.dart'
     as add_missing_enum_case_clauses;
+import 'add_missing_enum_like_case_clauses_test.dart'
+    as add_missing_enum_like_case_clauses;
 import 'add_missing_parameter_named_test.dart' as add_missing_parameter_named;
 import 'add_missing_parameter_positional_test.dart'
     as add_missing_parameter_positional;
@@ -193,6 +195,7 @@
     add_field_formal_parameters.main();
     add_late.main();
     add_missing_enum_case_clauses.main();
+    add_missing_enum_like_case_clauses.main();
     add_missing_parameter_named.main();
     add_missing_parameter_positional.main();
     add_missing_parameter_required.main();
diff --git a/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart b/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
index 077b56b..5bed787 100644
--- a/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
+++ b/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
@@ -6,14 +6,32 @@
 import 'package:analyzer/error/listener.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/src/generated/source.dart';
-import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
-import 'package:analyzer/src/util/file_paths.dart' as file_paths;
-import 'package:analyzer/src/util/yaml.dart';
-import 'package:path/path.dart' as path;
+import 'package:analyzer/src/pubspec/validators/dependency_validator.dart';
+import 'package:analyzer/src/pubspec/validators/flutter_validator.dart';
+import 'package:analyzer/src/pubspec/validators/name_validator.dart';
 import 'package:source_span/src/span.dart';
 import 'package:yaml/yaml.dart';
 
-class PubspecValidator {
+class BasePubspecValidator {
+  /// The resource provider used to access the file system.
+  final ResourceProvider provider;
+
+  /// The source representing the file being validated.
+  final Source source;
+
+  BasePubspecValidator(this.provider, this.source);
+
+  /// Report an error for the given node.
+  void reportErrorForNode(
+      ErrorReporter reporter, YamlNode node, ErrorCode errorCode,
+      [List<Object>? arguments]) {
+    SourceSpan span = node.span;
+    reporter.reportErrorForOffset(
+        errorCode, span.start.offset, span.length, arguments);
+  }
+}
+
+class PubspecField {
   /// The name of the sub-field (under `flutter`) whose value is a list of
   /// assets available to Flutter apps at runtime.
   static const String ASSETS_FIELD = 'assets';
@@ -42,16 +60,25 @@
 
   /// The name of the field whose value is the version of the package.
   static const String VERSION_FIELD = 'version';
+}
 
+class PubspecValidator {
   /// The resource provider used to access the file system.
   final ResourceProvider provider;
 
   /// The source representing the file being validated.
   final Source source;
 
+  final DependencyValidator _dependencyValidator;
+  final FlutterValidator _flutterValidator;
+  final NameValidator _nameValidator;
+
   /// Initialize a newly create validator to validate the content of the given
   /// [source].
-  PubspecValidator(this.provider, this.source);
+  PubspecValidator(this.provider, this.source)
+      : _dependencyValidator = DependencyValidator(provider, source),
+        _flutterValidator = FlutterValidator(provider, source),
+        _nameValidator = NameValidator(provider, source);
 
   /// Validate the given [contents].
   List<AnalysisError> validate(Map<dynamic, YamlNode> contents) {
@@ -62,226 +89,10 @@
       isNonNullableByDefault: false,
     );
 
-    _validateDependencies(reporter, contents);
-    _validateFlutter(reporter, contents);
-    _validateName(reporter, contents);
+    _dependencyValidator.validate(reporter, contents);
+    _flutterValidator.validate(reporter, contents);
+    _nameValidator.validate(reporter, contents);
 
     return recorder.errors;
   }
-
-  /// Return `true` if an asset (file) exists at the given absolute, normalized
-  /// [assetPath] or in a subdirectory of the parent of the file.
-  bool _assetExistsAtPath(String assetPath) {
-    // Check for asset directories.
-    Folder assetDirectory = provider.getFolder(assetPath);
-    if (assetDirectory.exists) {
-      return true;
-    }
-
-    // Else, check for an asset file.
-    File assetFile = provider.getFile(assetPath);
-    if (assetFile.exists) {
-      return true;
-    }
-    String fileName = assetFile.shortName;
-    Folder assetFolder = assetFile.parent2;
-    if (!assetFolder.exists) {
-      return false;
-    }
-    for (Resource child in assetFolder.getChildren()) {
-      if (child is Folder) {
-        File innerFile = child.getChildAssumingFile(fileName);
-        if (innerFile.exists) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  String? _asString(dynamic node) {
-    if (node is String) {
-      return node;
-    }
-    if (node is YamlScalar && node.value is String) {
-      return node.value as String;
-    }
-    return null;
-  }
-
-  /// Return a map whose keys are the names of declared dependencies and whose
-  /// values are the specifications of those dependencies. The map is extracted
-  /// from the given [contents] using the given [key].
-  Map<dynamic, YamlNode> _getDeclaredDependencies(
-      ErrorReporter reporter, Map<dynamic, YamlNode> contents, String key) {
-    var field = contents[key];
-    if (field == null || (field is YamlScalar && field.value == null)) {
-      return <String, YamlNode>{};
-    } else if (field is YamlMap) {
-      return field.nodes;
-    }
-    _reportErrorForNode(
-        reporter, field, PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP, [key]);
-    return <String, YamlNode>{};
-  }
-
-  /// Report an error for the given node.
-  void _reportErrorForNode(
-      ErrorReporter reporter, YamlNode node, ErrorCode errorCode,
-      [List<Object>? arguments]) {
-    SourceSpan span = node.span;
-    reporter.reportErrorForOffset(
-        errorCode, span.start.offset, span.length, arguments);
-  }
-
-  /// Validate the value of the required `name` field.
-  void _validateDependencies(
-      ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
-    Map<dynamic, YamlNode> declaredDependencies =
-        _getDeclaredDependencies(reporter, contents, DEPENDENCIES_FIELD);
-    Map<dynamic, YamlNode> declaredDevDependencies =
-        _getDeclaredDependencies(reporter, contents, DEV_DEPENDENCIES_FIELD);
-
-    bool isPublishablePackage = false;
-    var version = contents[VERSION_FIELD];
-    if (version != null) {
-      var publishTo = _asString(contents[PUBLISH_TO_FIELD]);
-      if (publishTo != 'none') {
-        isPublishablePackage = true;
-      }
-    }
-
-    for (var dependency in declaredDependencies.entries) {
-      _validatePathEntries(reporter, dependency.value, isPublishablePackage);
-    }
-
-    for (var dependency in declaredDevDependencies.entries) {
-      var packageName = dependency.key as YamlNode;
-      if (declaredDependencies.containsKey(packageName)) {
-        _reportErrorForNode(reporter, packageName,
-            PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY, [packageName.value]);
-      }
-      _validatePathEntries(reporter, dependency.value, false);
-    }
-  }
-
-  /// Validate the value of the optional `flutter` field.
-  void _validateFlutter(
-      ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
-    var flutterField = contents[FLUTTER_FIELD];
-    if (flutterField is YamlMap) {
-      var assetsField = flutterField.nodes[ASSETS_FIELD];
-      if (assetsField is YamlList) {
-        path.Context context = provider.pathContext;
-        String packageRoot = context.dirname(source.fullName);
-        for (YamlNode entryValue in assetsField.nodes) {
-          if (entryValue is YamlScalar) {
-            Object entry = entryValue.value;
-            if (entry is String) {
-              if (entry.startsWith('packages/')) {
-                // TODO(brianwilkerson) Add validation of package references.
-              } else {
-                bool isDirectoryEntry = entry.endsWith("/");
-                String normalizedEntry =
-                    context.joinAll(path.posix.split(entry));
-                String assetPath = context.join(packageRoot, normalizedEntry);
-                if (!_assetExistsAtPath(assetPath)) {
-                  ErrorCode errorCode = isDirectoryEntry
-                      ? PubspecWarningCode.ASSET_DIRECTORY_DOES_NOT_EXIST
-                      : PubspecWarningCode.ASSET_DOES_NOT_EXIST;
-                  _reportErrorForNode(
-                      reporter, entryValue, errorCode, [entryValue.value]);
-                }
-              }
-            } else {
-              _reportErrorForNode(
-                  reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING);
-            }
-          } else {
-            _reportErrorForNode(
-                reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING);
-          }
-        }
-      } else if (assetsField != null) {
-        _reportErrorForNode(
-            reporter, assetsField, PubspecWarningCode.ASSET_FIELD_NOT_LIST);
-      }
-
-      if (flutterField.length > 1) {
-        // TODO(brianwilkerson) Should we report an error if `flutter` contains
-        // keys other than `assets`?
-      }
-    } else if (flutterField != null) {
-      if (flutterField.value == null) {
-        // allow an empty `flutter:` section; explicitly fail on a non-empty,
-        // non-map one
-      } else {
-        _reportErrorForNode(
-            reporter, flutterField, PubspecWarningCode.FLUTTER_FIELD_NOT_MAP);
-      }
-    }
-  }
-
-  /// Validate the value of the required `name` field.
-  void _validateName(ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
-    var nameField = contents[NAME_FIELD];
-    if (nameField == null) {
-      reporter.reportErrorForOffset(PubspecWarningCode.MISSING_NAME, 0, 0);
-    } else if (nameField is! YamlScalar || nameField.value is! String) {
-      _reportErrorForNode(
-          reporter, nameField, PubspecWarningCode.NAME_NOT_STRING);
-    }
-  }
-
-  /// Validate that `path` entries reference valid paths.
-  ///
-  /// Valid paths are directories that:
-  ///
-  /// 1. exist,
-  /// 2. contain a pubspec.yaml file
-  ///
-  /// If [checkForPathAndGitDeps] is true, `git` or `path` dependencies will
-  /// be marked invalid.
-  void _validatePathEntries(ErrorReporter reporter, YamlNode dependency,
-      bool checkForPathAndGitDeps) {
-    if (dependency is YamlMap) {
-      var pathEntry = _asString(dependency[PATH_FIELD]);
-      if (pathEntry != null) {
-        YamlNode pathKey() => dependency.getKey(PATH_FIELD)!;
-        YamlNode pathValue() => dependency.valueAt(PATH_FIELD)!;
-
-        if (pathEntry.contains(r'\')) {
-          _reportErrorForNode(reporter, pathValue(),
-              PubspecWarningCode.PATH_NOT_POSIX, [pathEntry]);
-          return;
-        }
-        var context = provider.pathContext;
-        var normalizedPath = context.joinAll(path.posix.split(pathEntry));
-        var packageRoot = context.dirname(source.fullName);
-        var dependencyPath = context.join(packageRoot, normalizedPath);
-        dependencyPath = context.absolute(dependencyPath);
-        dependencyPath = context.normalize(dependencyPath);
-        var packageFolder = provider.getFolder(dependencyPath);
-        if (!packageFolder.exists) {
-          _reportErrorForNode(reporter, pathValue(),
-              PubspecWarningCode.PATH_DOES_NOT_EXIST, [pathEntry]);
-        } else {
-          if (!packageFolder.getChild(file_paths.pubspecYaml).exists) {
-            _reportErrorForNode(reporter, pathValue(),
-                PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST, [pathEntry]);
-          }
-        }
-        if (checkForPathAndGitDeps) {
-          _reportErrorForNode(reporter, pathKey(),
-              PubspecWarningCode.INVALID_DEPENDENCY, [PATH_FIELD]);
-        }
-      }
-
-      var gitEntry = dependency[GIT_FIELD];
-      if (gitEntry != null && checkForPathAndGitDeps) {
-        _reportErrorForNode(reporter, dependency.getKey(GIT_FIELD)!,
-            PubspecWarningCode.INVALID_DEPENDENCY, [GIT_FIELD]);
-      }
-    }
-  }
 }
diff --git a/pkg/analyzer/lib/src/pubspec/validators/dependency_validator.dart b/pkg/analyzer/lib/src/pubspec/validators/dependency_validator.dart
new file mode 100644
index 0000000..49f859c
--- /dev/null
+++ b/pkg/analyzer/lib/src/pubspec/validators/dependency_validator.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/pubspec/pubspec_validator.dart';
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:analyzer/src/util/yaml.dart';
+import 'package:path/path.dart' as path;
+import 'package:yaml/yaml.dart';
+
+class DependencyValidator extends BasePubspecValidator {
+  DependencyValidator(ResourceProvider provider, Source source)
+      : super(provider, source);
+
+  /// Validate the value of the required `name` field.
+  void validate(ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
+    Map<dynamic, YamlNode> declaredDependencies = _getDeclaredDependencies(
+        reporter, contents, PubspecField.DEPENDENCIES_FIELD);
+    Map<dynamic, YamlNode> declaredDevDependencies = _getDeclaredDependencies(
+        reporter, contents, PubspecField.DEV_DEPENDENCIES_FIELD);
+
+    bool isPublishablePackage = false;
+    var version = contents[PubspecField.VERSION_FIELD];
+    if (version != null) {
+      var publishTo = _asString(contents[PubspecField.PUBLISH_TO_FIELD]);
+      if (publishTo != 'none') {
+        isPublishablePackage = true;
+      }
+    }
+
+    for (var dependency in declaredDependencies.entries) {
+      _validatePathEntries(reporter, dependency.value, isPublishablePackage);
+    }
+
+    for (var dependency in declaredDevDependencies.entries) {
+      var packageName = dependency.key as YamlNode;
+      if (declaredDependencies.containsKey(packageName)) {
+        reportErrorForNode(reporter, packageName,
+            PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY, [packageName.value]);
+      }
+      _validatePathEntries(reporter, dependency.value, false);
+    }
+  }
+
+  String? _asString(dynamic node) {
+    if (node is String) {
+      return node;
+    }
+    if (node is YamlScalar && node.value is String) {
+      return node.value as String;
+    }
+    return null;
+  }
+
+  /// Return a map whose keys are the names of declared dependencies and whose
+  /// values are the specifications of those dependencies. The map is extracted
+  /// from the given [contents] using the given [key].
+  Map<dynamic, YamlNode> _getDeclaredDependencies(
+      ErrorReporter reporter, Map<dynamic, YamlNode> contents, String key) {
+    var field = contents[key];
+    if (field == null || (field is YamlScalar && field.value == null)) {
+      return <String, YamlNode>{};
+    } else if (field is YamlMap) {
+      return field.nodes;
+    }
+    reportErrorForNode(
+        reporter, field, PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP, [key]);
+    return <String, YamlNode>{};
+  }
+
+  /// Validate that `path` entries reference valid paths.
+  ///
+  /// Valid paths are directories that:
+  ///
+  /// 1. exist,
+  /// 2. contain a pubspec.yaml file
+  ///
+  /// If [checkForPathAndGitDeps] is true, `git` or `path` dependencies will
+  /// be marked invalid.
+  void _validatePathEntries(ErrorReporter reporter, YamlNode dependency,
+      bool checkForPathAndGitDeps) {
+    if (dependency is YamlMap) {
+      var pathEntry = _asString(dependency[PubspecField.PATH_FIELD]);
+      if (pathEntry != null) {
+        YamlNode pathKey() => dependency.getKey(PubspecField.PATH_FIELD)!;
+        YamlNode pathValue() => dependency.valueAt(PubspecField.PATH_FIELD)!;
+
+        if (pathEntry.contains(r'\')) {
+          reportErrorForNode(reporter, pathValue(),
+              PubspecWarningCode.PATH_NOT_POSIX, [pathEntry]);
+          return;
+        }
+        var context = provider.pathContext;
+        var normalizedPath = context.joinAll(path.posix.split(pathEntry));
+        var packageRoot = context.dirname(source.fullName);
+        var dependencyPath = context.join(packageRoot, normalizedPath);
+        dependencyPath = context.absolute(dependencyPath);
+        dependencyPath = context.normalize(dependencyPath);
+        var packageFolder = provider.getFolder(dependencyPath);
+        if (!packageFolder.exists) {
+          reportErrorForNode(reporter, pathValue(),
+              PubspecWarningCode.PATH_DOES_NOT_EXIST, [pathEntry]);
+        } else {
+          if (!packageFolder.getChild(file_paths.pubspecYaml).exists) {
+            reportErrorForNode(reporter, pathValue(),
+                PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST, [pathEntry]);
+          }
+        }
+        if (checkForPathAndGitDeps) {
+          reportErrorForNode(reporter, pathKey(),
+              PubspecWarningCode.INVALID_DEPENDENCY, [PubspecField.PATH_FIELD]);
+        }
+      }
+
+      var gitEntry = dependency[PubspecField.GIT_FIELD];
+      if (gitEntry != null && checkForPathAndGitDeps) {
+        reportErrorForNode(reporter, dependency.getKey(PubspecField.GIT_FIELD)!,
+            PubspecWarningCode.INVALID_DEPENDENCY, [PubspecField.GIT_FIELD]);
+      }
+    }
+  }
+}
diff --git a/pkg/analyzer/lib/src/pubspec/validators/flutter_validator.dart b/pkg/analyzer/lib/src/pubspec/validators/flutter_validator.dart
new file mode 100644
index 0000000..bb8803b
--- /dev/null
+++ b/pkg/analyzer/lib/src/pubspec/validators/flutter_validator.dart
@@ -0,0 +1,103 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/pubspec/pubspec_validator.dart';
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:path/path.dart' as path;
+import 'package:yaml/yaml.dart';
+
+class FlutterValidator extends BasePubspecValidator {
+  FlutterValidator(ResourceProvider provider, Source source)
+      : super(provider, source);
+
+  /// Validate the value of the optional `flutter` field.
+  void validate(ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
+    var flutterField = contents[PubspecField.FLUTTER_FIELD];
+    if (flutterField is YamlMap) {
+      var assetsField = flutterField.nodes[PubspecField.ASSETS_FIELD];
+      if (assetsField is YamlList) {
+        path.Context context = provider.pathContext;
+        String packageRoot = context.dirname(source.fullName);
+        for (YamlNode entryValue in assetsField.nodes) {
+          if (entryValue is YamlScalar) {
+            Object entry = entryValue.value;
+            if (entry is String) {
+              if (entry.startsWith('packages/')) {
+                // TODO(brianwilkerson) Add validation of package references.
+              } else {
+                bool isDirectoryEntry = entry.endsWith("/");
+                String normalizedEntry =
+                    context.joinAll(path.posix.split(entry));
+                String assetPath = context.join(packageRoot, normalizedEntry);
+                if (!_assetExistsAtPath(assetPath)) {
+                  ErrorCode errorCode = isDirectoryEntry
+                      ? PubspecWarningCode.ASSET_DIRECTORY_DOES_NOT_EXIST
+                      : PubspecWarningCode.ASSET_DOES_NOT_EXIST;
+                  reportErrorForNode(
+                      reporter, entryValue, errorCode, [entryValue.value]);
+                }
+              }
+            } else {
+              reportErrorForNode(
+                  reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING);
+            }
+          } else {
+            reportErrorForNode(
+                reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING);
+          }
+        }
+      } else if (assetsField != null) {
+        reportErrorForNode(
+            reporter, assetsField, PubspecWarningCode.ASSET_FIELD_NOT_LIST);
+      }
+
+      if (flutterField.length > 1) {
+        // TODO(brianwilkerson) Should we report an error if `flutter` contains
+        // keys other than `assets`?
+      }
+    } else if (flutterField != null) {
+      if (flutterField.value == null) {
+        // allow an empty `flutter:` section; explicitly fail on a non-empty,
+        // non-map one
+      } else {
+        reportErrorForNode(
+            reporter, flutterField, PubspecWarningCode.FLUTTER_FIELD_NOT_MAP);
+      }
+    }
+  }
+
+  /// Return `true` if an asset (file) exists at the given absolute, normalized
+  /// [assetPath] or in a subdirectory of the parent of the file.
+  bool _assetExistsAtPath(String assetPath) {
+    // Check for asset directories.
+    Folder assetDirectory = provider.getFolder(assetPath);
+    if (assetDirectory.exists) {
+      return true;
+    }
+
+    // Else, check for an asset file.
+    File assetFile = provider.getFile(assetPath);
+    if (assetFile.exists) {
+      return true;
+    }
+    String fileName = assetFile.shortName;
+    Folder assetFolder = assetFile.parent2;
+    if (!assetFolder.exists) {
+      return false;
+    }
+    for (Resource child in assetFolder.getChildren()) {
+      if (child is Folder) {
+        File innerFile = child.getChildAssumingFile(fileName);
+        if (innerFile.exists) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/pkg/analyzer/lib/src/pubspec/validators/name_validator.dart b/pkg/analyzer/lib/src/pubspec/validators/name_validator.dart
new file mode 100644
index 0000000..9564ab7
--- /dev/null
+++ b/pkg/analyzer/lib/src/pubspec/validators/name_validator.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/pubspec/pubspec_validator.dart';
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:yaml/yaml.dart';
+
+class NameValidator extends BasePubspecValidator {
+  NameValidator(ResourceProvider provider, Source source)
+      : super(provider, source);
+
+  /// Validate the value of the required `name` field.
+  void validate(ErrorReporter reporter, Map<dynamic, YamlNode> contents) {
+    var nameField = contents[PubspecField.NAME_FIELD];
+    if (nameField == null) {
+      reporter.reportErrorForOffset(PubspecWarningCode.MISSING_NAME, 0, 0);
+    } else if (nameField is! YamlScalar || nameField.value is! String) {
+      reportErrorForNode(
+          reporter, nameField, PubspecWarningCode.NAME_NOT_STRING);
+    }
+  }
+}
diff --git a/pkg/analyzer/test/src/pubspec/pubspec_test_support.dart b/pkg/analyzer/test/src/pubspec/pubspec_test_support.dart
new file mode 100644
index 0000000..e2816d5
--- /dev/null
+++ b/pkg/analyzer/test/src/pubspec/pubspec_test_support.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/pubspec/pubspec_validator.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+import '../../generated/test_support.dart';
+
+class BasePubspecValidatorTest with ResourceProviderMixin {
+  late PubspecValidator validator;
+
+  /// Assert that when the validator is used on the given [content] the
+  /// [expectedErrorCodes] are produced.
+  void assertErrors(String content, List<ErrorCode> expectedErrorCodes) {
+    YamlNode node = loadYamlNode(content);
+    if (node is! YamlMap) {
+      // The file is empty.
+      node = YamlMap();
+    }
+    List<AnalysisError> errors = validator.validate(node.nodes);
+    GatheringErrorListener listener = GatheringErrorListener();
+    listener.addAll(errors);
+    listener.assertErrorsWithCodes(expectedErrorCodes);
+  }
+
+  /// Assert that when the validator is used on the given [content] no errors
+  /// are produced.
+  void assertNoErrors(String content) {
+    assertErrors(content, []);
+  }
+
+  @mustCallSuper
+  void setUp() {
+    var pubspecFile = getFile('/sample/pubspec.yaml');
+    var source = pubspecFile.createSource();
+    validator = PubspecValidator(resourceProvider, source);
+  }
+}
diff --git a/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart b/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart
deleted file mode 100644
index 005d55f..0000000
--- a/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart
+++ /dev/null
@@ -1,544 +0,0 @@
-// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:analyzer/error/error.dart';
-import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/src/generated/source.dart';
-import 'package:analyzer/src/pubspec/pubspec_validator.dart';
-import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
-import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
-import 'package:test_reflective_loader/test_reflective_loader.dart';
-import 'package:yaml/yaml.dart';
-
-import '../../generated/test_support.dart';
-
-main() {
-  defineReflectiveSuite(() {
-    defineReflectiveTests(PubspecValidatorTest);
-  });
-}
-
-@reflectiveTest
-class PubspecValidatorTest with ResourceProviderMixin {
-  late final PubspecValidator validator;
-
-  /// Assert that when the validator is used on the given [content] the
-  /// [expectedErrorCodes] are produced.
-  void assertErrors(String content, List<ErrorCode> expectedErrorCodes) {
-    YamlNode node = loadYamlNode(content);
-    if (node is! YamlMap) {
-      // The file is empty.
-      node = YamlMap();
-    }
-    List<AnalysisError> errors = validator.validate(node.nodes);
-    GatheringErrorListener listener = GatheringErrorListener();
-    listener.addAll(errors);
-    listener.assertErrorsWithCodes(expectedErrorCodes);
-  }
-
-  /// Assert that when the validator is used on the given [content] no errors
-  /// are produced.
-  void assertNoErrors(String content) {
-    assertErrors(content, []);
-  }
-
-  void setUp() {
-    File pubspecFile = getFile('/sample/pubspec.yaml');
-    Source source = pubspecFile.createSource();
-    validator = PubspecValidator(resourceProvider, source);
-  }
-
-  test_assetDirectoryDoesExist_noError() {
-    newFolder('/sample/assets/logos');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/logos/
-''');
-  }
-
-  test_assetDirectoryDoesNotExist_error() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/logos/
-''', [PubspecWarningCode.ASSET_DIRECTORY_DOES_NOT_EXIST]);
-  }
-
-  test_assetDoesNotExist_path_error() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/my_icon.png
-''', [PubspecWarningCode.ASSET_DOES_NOT_EXIST]);
-  }
-
-  test_assetDoesNotExist_path_inRoot_noError() {
-    newFile('/sample/assets/my_icon.png');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/my_icon.png
-''');
-  }
-
-  test_assetDoesNotExist_path_inSubdir_noError() {
-    newFile('/sample/assets/images/2.0x/my_icon.png');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/images/my_icon.png
-''');
-  }
-
-  @failingTest
-  test_assetDoesNotExist_uri_error() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-    - packages/icons/my_icon.png
-''', [PubspecWarningCode.ASSET_DOES_NOT_EXIST]);
-  }
-
-  test_assetDoesNotExist_uri_noError() {
-    // TODO(brianwilkerson) Create a package named `icons` that contains the
-    // referenced file, and a `.packages` file that references that package.
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - packages/icons/my_icon.png
-''');
-  }
-
-  test_assetFieldNotList_error_empty() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-''', [PubspecWarningCode.ASSET_FIELD_NOT_LIST]);
-  }
-
-  test_assetFieldNotList_error_string() {
-    assertErrors('''
-name: sample
-flutter:
-  assets: assets/my_icon.png
-''', [PubspecWarningCode.ASSET_FIELD_NOT_LIST]);
-  }
-
-  test_assetFieldNotList_noError() {
-    newFile('/sample/assets/my_icon.png');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/my_icon.png
-''');
-  }
-
-  test_assetNotString_error_int() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-    - 23
-''', [PubspecWarningCode.ASSET_NOT_STRING]);
-  }
-
-  test_assetNotString_error_map() {
-    assertErrors('''
-name: sample
-flutter:
-  assets:
-    - my_icon:
-      default: assets/my_icon.png
-      large: assets/large/my_icon.png
-''', [PubspecWarningCode.ASSET_NOT_STRING]);
-  }
-
-  test_assetNotString_noError() {
-    newFile('/sample/assets/my_icon.png');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/my_icon.png
-''');
-  }
-
-  test_dependenciesField_empty() {
-    assertNoErrors('''
-name: sample
-dependencies:
-''');
-  }
-
-  test_dependenciesFieldNotMap_error_bool() {
-    assertErrors('''
-name: sample
-dependencies: true
-''', [PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP]);
-  }
-
-  test_dependenciesFieldNotMap_noError() {
-    assertNoErrors('''
-name: sample
-dependencies:
-  a: any
-''');
-  }
-
-  test_dependencyGit_malformed_empty() {
-    // todo (pq): consider validating.
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    git:
-''');
-  }
-
-  test_dependencyGit_malformed_list() {
-    // todo (pq): consider validating.
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    git:
-      - baz
-''');
-  }
-
-  test_dependencyGit_malformed_scalar() {
-    // todo (pq): consider validating.
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    git: baz
-''');
-  }
-
-  test_dependencyGit_noVersion_valid() {
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    git:
-      url: git@github.com:foo/foo.git
-      path: path/to/foo
-''');
-  }
-
-  test_dependencyGit_version_error() {
-    assertErrors('''
-name: sample
-version: 0.1.0
-dependencies:
-  foo:
-    git:
-      url: git@github.com:foo/foo.git
-      path: path/to/foo
-''', [PubspecWarningCode.INVALID_DEPENDENCY]);
-  }
-
-  test_dependencyGit_version_valid() {
-    assertNoErrors('''
-name: sample
-version: 0.1.0
-publish_to: none
-dependencies:
-  foo:
-    git:
-      url: git@github.com:foo/foo.git
-      path: path/to/foo
-''');
-  }
-
-  test_dependencyGitPath() {
-    // git paths are not validated
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    git:
-      url: git@github.com:foo/foo.git
-      path: path/to/foo
-''');
-  }
-
-  test_dependencyPath_malformed_empty() {
-    // todo (pq): consider validating.
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path:
-''');
-  }
-
-  test_dependencyPath_malformed_list() {
-    // todo (pq): consider validating.
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path:
-     - baz
-''');
-  }
-
-  test_dependencyPath_noVersion_valid() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path: /foo
-''');
-  }
-
-  test_dependencyPath_pubspecDoesNotExist() {
-    newFolder('/foo');
-    assertErrors('''
-name: sample
-dependencies:
-  foo:
-    path: /foo
-''', [PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST]);
-  }
-
-  test_dependencyPath_pubspecExists() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path: /foo
-''');
-  }
-
-  test_dependencyPath_valid_absolute() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path: /foo
-''');
-  }
-
-  test_dependencyPath_valid_relative() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-dependencies:
-  foo:
-    path: ../foo
-''');
-  }
-
-  test_dependencyPath_version_error() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertErrors('''
-name: sample
-version: 0.1.0
-dependencies:
-  foo:
-    path: /foo
-''', [PubspecWarningCode.INVALID_DEPENDENCY]);
-  }
-
-  test_dependencyPath_version_valid() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-version: 0.1.0
-publish_to: none
-dependencies:
-  foo:
-    path: /foo
-''');
-  }
-
-  test_dependencyPathDoesNotExist_path_error() {
-    assertErrors('''
-name: sample
-dependencies:
-  foo:
-    path: does/not/exist
-''', [PubspecWarningCode.PATH_DOES_NOT_EXIST]);
-  }
-
-  test_devDependenciesField_empty() {
-    assertNoErrors('''
-name: sample
-dev_dependencies:
-''');
-  }
-
-  test_devDependenciesFieldNotMap_dev_error_bool() {
-    assertErrors('''
-name: sample
-dev_dependencies: true
-''', [PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP]);
-  }
-
-  test_devDependenciesFieldNotMap_dev_noError() {
-    assertNoErrors('''
-name: sample
-dev_dependencies:
-  a: any
-''');
-  }
-
-  test_devDependencyGit_version_no_error() {
-    // Git paths are OK in dev_dependencies
-    assertNoErrors('''
-name: sample
-version: 0.1.0
-dev_dependencies:
-  foo:
-    git:
-      url: git@github.com:foo/foo.git
-      path: path/to/foo
-''');
-  }
-
-  test_devDependencyPathDoesNotExist_path_error() {
-    assertErrors('''
-name: sample
-dev_dependencies:
-  foo:
-    path: does/not/exist
-''', [PubspecWarningCode.PATH_DOES_NOT_EXIST]);
-  }
-
-  test_devDependencyPathExists() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertNoErrors('''
-name: sample
-dev_dependencies:
-  foo:
-    path: /foo
-''');
-  }
-
-  test_flutterField_empty_noError() {
-    assertNoErrors('''
-name: sample
-flutter:
-''');
-
-    assertNoErrors('''
-name: sample
-flutter:
-
-''');
-  }
-
-  test_flutterFieldNotMap_error_bool() {
-    assertErrors('''
-name: sample
-flutter: true
-''', [PubspecWarningCode.FLUTTER_FIELD_NOT_MAP]);
-  }
-
-  test_flutterFieldNotMap_noError() {
-    newFile('/sample/assets/my_icon.png');
-    assertNoErrors('''
-name: sample
-flutter:
-  assets:
-    - assets/my_icon.png
-''');
-  }
-
-  test_missingName_error() {
-    assertErrors('', [PubspecWarningCode.MISSING_NAME]);
-  }
-
-  test_missingName_noError() {
-    assertNoErrors('''
-name: sample
-''');
-  }
-
-  test_nameNotString_error_int() {
-    assertErrors('''
-name: 42
-''', [PubspecWarningCode.NAME_NOT_STRING]);
-  }
-
-  test_nameNotString_noError() {
-    assertNoErrors('''
-name: sample
-''');
-  }
-
-  test_pathNotPosix_error() {
-    newFolder('/foo');
-    newPubspecYamlFile('/foo', '''
-name: foo
-''');
-    assertErrors(r'''
-name: sample
-version: 0.1.0
-publish_to: none
-dependencies:
-  foo:
-    path: \foo
-''', [
-      PubspecWarningCode.PATH_NOT_POSIX,
-    ]);
-  }
-
-  test_unnecessaryDevDependency_error() {
-    assertErrors('''
-name: sample
-dependencies:
-  a: any
-dev_dependencies:
-  a: any
-''', [PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY]);
-  }
-
-  test_unnecessaryDevDependency_noError() {
-    assertNoErrors('''
-name: sample
-dependencies:
-  a: any
-dev_dependencies:
-  b: any
-''');
-  }
-}
diff --git a/pkg/analyzer/test/src/pubspec/test_all.dart b/pkg/analyzer/test/src/pubspec/test_all.dart
index c4aa6ea..d5ef1de 100644
--- a/pkg/analyzer/test/src/pubspec/test_all.dart
+++ b/pkg/analyzer/test/src/pubspec/test_all.dart
@@ -4,10 +4,10 @@
 
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
-import 'pubspec_validator_test.dart' as pubspec_validator;
+import 'validators/test_all.dart' as validator_tests;
 
 main() {
   defineReflectiveSuite(() {
-    pubspec_validator.main();
+    validator_tests.main();
   }, name: 'pubspec');
 }
diff --git a/pkg/analyzer/test/src/pubspec/validators/pubspec_dependency_validator_test.dart b/pkg/analyzer/test/src/pubspec/validators/pubspec_dependency_validator_test.dart
new file mode 100644
index 0000000..cb52a95
--- /dev/null
+++ b/pkg/analyzer/test/src/pubspec/validators/pubspec_dependency_validator_test.dart
@@ -0,0 +1,333 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../pubspec_test_support.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(PubspecDependencyValidatorTest);
+  });
+}
+
+@reflectiveTest
+class PubspecDependencyValidatorTest extends BasePubspecValidatorTest {
+  test_dependenciesField_empty() {
+    assertNoErrors('''
+name: sample
+dependencies:
+''');
+  }
+
+  test_dependenciesFieldNotMap_error_bool() {
+    assertErrors('''
+name: sample
+dependencies: true
+''', [PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP]);
+  }
+
+  test_dependenciesFieldNotMap_noError() {
+    assertNoErrors('''
+name: sample
+dependencies:
+  a: any
+''');
+  }
+
+  test_dependencyGit_malformed_empty() {
+    // todo (pq): consider validating.
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    git:
+''');
+  }
+
+  test_dependencyGit_malformed_list() {
+    // todo (pq): consider validating.
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    git:
+      - baz
+''');
+  }
+
+  test_dependencyGit_malformed_scalar() {
+    // todo (pq): consider validating.
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    git: baz
+''');
+  }
+
+  test_dependencyGit_noVersion_valid() {
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    git:
+      url: git@github.com:foo/foo.git
+      path: path/to/foo
+''');
+  }
+
+  test_dependencyGit_version_error() {
+    assertErrors('''
+name: sample
+version: 0.1.0
+dependencies:
+  foo:
+    git:
+      url: git@github.com:foo/foo.git
+      path: path/to/foo
+''', [PubspecWarningCode.INVALID_DEPENDENCY]);
+  }
+
+  test_dependencyGit_version_valid() {
+    assertNoErrors('''
+name: sample
+version: 0.1.0
+publish_to: none
+dependencies:
+  foo:
+    git:
+      url: git@github.com:foo/foo.git
+      path: path/to/foo
+''');
+  }
+
+  test_dependencyGitPath() {
+    // git paths are not validated
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    git:
+      url: git@github.com:foo/foo.git
+      path: path/to/foo
+''');
+  }
+
+  test_dependencyPath_malformed_empty() {
+    // todo (pq): consider validating.
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path:
+''');
+  }
+
+  test_dependencyPath_malformed_list() {
+    // todo (pq): consider validating.
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path:
+     - baz
+''');
+  }
+
+  test_dependencyPath_noVersion_valid() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
+  test_dependencyPath_pubspecDoesNotExist() {
+    newFolder('/foo');
+    assertErrors('''
+name: sample
+dependencies:
+  foo:
+    path: /foo
+''', [PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST]);
+  }
+
+  test_dependencyPath_pubspecExists() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
+  test_dependencyPath_valid_absolute() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
+  test_dependencyPath_valid_relative() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path: ../foo
+''');
+  }
+
+  test_dependencyPath_version_error() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertErrors('''
+name: sample
+version: 0.1.0
+dependencies:
+  foo:
+    path: /foo
+''', [PubspecWarningCode.INVALID_DEPENDENCY]);
+  }
+
+  test_dependencyPath_version_valid() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+version: 0.1.0
+publish_to: none
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
+  test_dependencyPathDoesNotExist_path_error() {
+    assertErrors('''
+name: sample
+dependencies:
+  foo:
+    path: does/not/exist
+''', [PubspecWarningCode.PATH_DOES_NOT_EXIST]);
+  }
+
+  test_devDependenciesField_empty() {
+    assertNoErrors('''
+name: sample
+dev_dependencies:
+''');
+  }
+
+  test_devDependenciesFieldNotMap_dev_error_bool() {
+    assertErrors('''
+name: sample
+dev_dependencies: true
+''', [PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP]);
+  }
+
+  test_devDependenciesFieldNotMap_dev_noError() {
+    assertNoErrors('''
+name: sample
+dev_dependencies:
+  a: any
+''');
+  }
+
+  test_devDependencyGit_version_no_error() {
+    // Git paths are OK in dev_dependencies
+    assertNoErrors('''
+name: sample
+version: 0.1.0
+dev_dependencies:
+  foo:
+    git:
+      url: git@github.com:foo/foo.git
+      path: path/to/foo
+''');
+  }
+
+  test_devDependencyPathDoesNotExist_path_error() {
+    assertErrors('''
+name: sample
+dev_dependencies:
+  foo:
+    path: does/not/exist
+''', [PubspecWarningCode.PATH_DOES_NOT_EXIST]);
+  }
+
+  test_devDependencyPathExists() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dev_dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
+  test_pathNotPosix_error() {
+    newFolder('/foo');
+    newPubspecYamlFile('/foo', '''
+name: foo
+''');
+    assertErrors(r'''
+name: sample
+version: 0.1.0
+publish_to: none
+dependencies:
+  foo:
+    path: \foo
+''', [
+      PubspecWarningCode.PATH_NOT_POSIX,
+    ]);
+  }
+
+  test_unnecessaryDevDependency_error() {
+    assertErrors('''
+name: sample
+dependencies:
+  a: any
+dev_dependencies:
+  a: any
+''', [PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY]);
+  }
+
+  test_unnecessaryDevDependency_noError() {
+    assertNoErrors('''
+name: sample
+dependencies:
+  a: any
+dev_dependencies:
+  b: any
+''');
+  }
+}
diff --git a/pkg/analyzer/test/src/pubspec/validators/pubspec_flutter_validator_test.dart b/pkg/analyzer/test/src/pubspec/validators/pubspec_flutter_validator_test.dart
new file mode 100644
index 0000000..34c9489
--- /dev/null
+++ b/pkg/analyzer/test/src/pubspec/validators/pubspec_flutter_validator_test.dart
@@ -0,0 +1,172 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../pubspec_test_support.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(PubspecFlutterValidatorTest);
+  });
+}
+
+@reflectiveTest
+class PubspecFlutterValidatorTest extends BasePubspecValidatorTest {
+  test_assetDirectoryDoesExist_noError() {
+    newFolder('/sample/assets/logos');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/logos/
+''');
+  }
+
+  test_assetDirectoryDoesNotExist_error() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/logos/
+''', [PubspecWarningCode.ASSET_DIRECTORY_DOES_NOT_EXIST]);
+  }
+
+  test_assetDoesNotExist_path_error() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/my_icon.png
+''', [PubspecWarningCode.ASSET_DOES_NOT_EXIST]);
+  }
+
+  test_assetDoesNotExist_path_inRoot_noError() {
+    newFile('/sample/assets/my_icon.png');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/my_icon.png
+''');
+  }
+
+  test_assetDoesNotExist_path_inSubdir_noError() {
+    newFile('/sample/assets/images/2.0x/my_icon.png');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/images/my_icon.png
+''');
+  }
+
+  @failingTest
+  test_assetDoesNotExist_uri_error() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+    - packages/icons/my_icon.png
+''', [PubspecWarningCode.ASSET_DOES_NOT_EXIST]);
+  }
+
+  test_assetDoesNotExist_uri_noError() {
+    // TODO(brianwilkerson) Create a package named `icons` that contains the
+    // referenced file, and a `.packages` file that references that package.
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - packages/icons/my_icon.png
+''');
+  }
+
+  test_assetFieldNotList_error_empty() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+''', [PubspecWarningCode.ASSET_FIELD_NOT_LIST]);
+  }
+
+  test_assetFieldNotList_error_string() {
+    assertErrors('''
+name: sample
+flutter:
+  assets: assets/my_icon.png
+''', [PubspecWarningCode.ASSET_FIELD_NOT_LIST]);
+  }
+
+  test_assetFieldNotList_noError() {
+    newFile('/sample/assets/my_icon.png');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/my_icon.png
+''');
+  }
+
+  test_assetNotString_error_int() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+    - 23
+''', [PubspecWarningCode.ASSET_NOT_STRING]);
+  }
+
+  test_assetNotString_error_map() {
+    assertErrors('''
+name: sample
+flutter:
+  assets:
+    - my_icon:
+      default: assets/my_icon.png
+      large: assets/large/my_icon.png
+''', [PubspecWarningCode.ASSET_NOT_STRING]);
+  }
+
+  test_assetNotString_noError() {
+    newFile('/sample/assets/my_icon.png');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/my_icon.png
+''');
+  }
+
+  test_flutterField_empty_noError() {
+    assertNoErrors('''
+name: sample
+flutter:
+''');
+
+    assertNoErrors('''
+name: sample
+flutter:
+
+''');
+  }
+
+  test_flutterFieldNotMap_error_bool() {
+    assertErrors('''
+name: sample
+flutter: true
+''', [PubspecWarningCode.FLUTTER_FIELD_NOT_MAP]);
+  }
+
+  test_flutterFieldNotMap_noError() {
+    newFile('/sample/assets/my_icon.png');
+    assertNoErrors('''
+name: sample
+flutter:
+  assets:
+    - assets/my_icon.png
+''');
+  }
+}
diff --git a/pkg/analyzer/test/src/pubspec/validators/pubspec_name_validator_test.dart b/pkg/analyzer/test/src/pubspec/validators/pubspec_name_validator_test.dart
new file mode 100644
index 0000000..71a7b97
--- /dev/null
+++ b/pkg/analyzer/test/src/pubspec/validators/pubspec_name_validator_test.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../pubspec_test_support.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(PubspecNameValidatorTest);
+  });
+}
+
+@reflectiveTest
+class PubspecNameValidatorTest extends BasePubspecValidatorTest {
+  test_missingName_error() {
+    assertErrors('', [PubspecWarningCode.MISSING_NAME]);
+  }
+
+  test_missingName_noError() {
+    assertNoErrors('''
+name: sample
+''');
+  }
+
+  test_nameNotString_error_int() {
+    assertErrors('''
+name: 42
+''', [PubspecWarningCode.NAME_NOT_STRING]);
+  }
+
+  test_nameNotString_noError() {
+    assertNoErrors('''
+name: sample
+''');
+  }
+}
diff --git a/pkg/analyzer/test/src/pubspec/validators/test_all.dart b/pkg/analyzer/test/src/pubspec/validators/test_all.dart
new file mode 100644
index 0000000..cd7a55c
--- /dev/null
+++ b/pkg/analyzer/test/src/pubspec/validators/test_all.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'pubspec_dependency_validator_test.dart'
+    as pubspec_dependency_validator_test;
+import 'pubspec_flutter_validator_test.dart' as pubspec_flutter_validator_test;
+import 'pubspec_name_validator_test.dart' as pubspec_name_validator_test;
+
+main() {
+  defineReflectiveSuite(() {
+    pubspec_dependency_validator_test.main();
+    pubspec_flutter_validator_test.main();
+    pubspec_name_validator_test.main();
+  }, name: 'validators');
+}
diff --git a/tools/VERSION b/tools/VERSION
index 6b36631..bcc1746 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 227
+PRERELEASE 228
 PRERELEASE_PATCH 0
\ No newline at end of file