check for invalid git and path deps in publishable packages

Fixes: https://github.com/dart-lang/sdk/issues/43930

Change-Id: Ia1e7899cf04644387f2686e997e338baa197dac3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/169246
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
diff --git a/pkg/analyzer/lib/error/error.dart b/pkg/analyzer/lib/error/error.dart
index 30ed3d0..0ef80e5 100644
--- a/pkg/analyzer/lib/error/error.dart
+++ b/pkg/analyzer/lib/error/error.dart
@@ -16,6 +16,7 @@
 import 'package:analyzer/src/generated/parser.dart' show ParserErrorCode;
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/manifest/manifest_warning_code.dart';
+import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
 
 export 'package:_fe_analyzer_shared/src/base/errors.dart'
     show ErrorCode, ErrorSeverity, ErrorType;
@@ -795,6 +796,9 @@
   ParserErrorCode.WITH_BEFORE_EXTENDS,
   ParserErrorCode.WRONG_SEPARATOR_FOR_POSITIONAL_PARAMETER,
   ParserErrorCode.WRONG_TERMINATOR_FOR_PARAMETER_GROUP,
+  PubspecWarningCode.INVALID_DEPENDENCY,
+  PubspecWarningCode.PATH_DOES_NOT_EXIST,
+  PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST,
   ScannerErrorCode.EXPECTED_TOKEN,
   ScannerErrorCode.ILLEGAL_CHARACTER,
   ScannerErrorCode.MISSING_DIGIT,
diff --git a/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart b/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
index 83c2031..af82bcf 100644
--- a/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
+++ b/pkg/analyzer/lib/src/pubspec/pubspec_validator.dart
@@ -8,6 +8,7 @@
 import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
+import 'package:analyzer/src/util/yaml.dart';
 import 'package:path/path.dart' as path;
 import 'package:source_span/src/span.dart';
 import 'package:yaml/yaml.dart';
@@ -27,12 +28,21 @@
   /// configuration data.
   static const String FLUTTER_FIELD = 'flutter';
 
+  /// The name of the field whose value is a git dependency.
+  static const String GIT_FIELD = 'git';
+
   /// The name of the field whose value is the name of the package.
   static const String NAME_FIELD = 'name';
 
   /// The name of the field whose value is a path to a package dependency.
   static const String PATH_FIELD = 'path';
 
+  /// The name of the field whose value is the where to publish the package.
+  static const String PUBLISH_TO_FIELD = 'publish_to';
+
+  /// The name of the field whose value is the version of the package.
+  static const String VERSION_FIELD = 'version';
+
   /// The resource provider used to access the file system.
   final ResourceProvider provider;
 
@@ -89,6 +99,16 @@
     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].
@@ -122,8 +142,17 @@
     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);
+      _validatePathEntries(reporter, dependency.value, isPublishablePackage);
     }
 
     for (var dependency in declaredDevDependencies.entries) {
@@ -132,7 +161,7 @@
         _reportErrorForNode(reporter, packageName,
             PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY, [packageName.value]);
       }
-      _validatePathEntries(reporter, dependency.value);
+      _validatePathEntries(reporter, dependency.value, false);
     }
   }
 
@@ -209,33 +238,44 @@
   /// Valid paths are directories that:
   ///
   /// 1. exist,
-  /// 2. contain a pubspec.yaml file, and
-  /// 3. `lib` dir
-  void _validatePathEntries(ErrorReporter reporter, YamlNode dependency) {
+  /// 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) {
-      for (var node in dependency.nodes.entries) {
-        var pathEntry = dependency[PATH_FIELD];
-        if (pathEntry != null) {
-          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, node.value,
-                PubspecWarningCode.PATH_DOES_NOT_EXIST, [pathEntry]);
-          } else {
-            if (!packageFolder
-                .getChild(AnalysisEngine.PUBSPEC_YAML_FILE)
-                .exists) {
-              _reportErrorForNode(reporter, node.value,
-                  PubspecWarningCode.PATH_PUBSPEC_DOES_NOT_EXIST, [pathEntry]);
-            }
-            // todo (pq): test for presence of a lib dir.
+      var pathEntry = _asString(dependency[PATH_FIELD]);
+      if (pathEntry != null) {
+        YamlNode pathKey() => getKey(dependency, PATH_FIELD);
+        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, pathKey(),
+              PubspecWarningCode.PATH_DOES_NOT_EXIST, [pathEntry]);
+        } else {
+          if (!packageFolder
+              .getChild(AnalysisEngine.PUBSPEC_YAML_FILE)
+              .exists) {
+            _reportErrorForNode(reporter, pathKey(),
+                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, getKey(dependency, GIT_FIELD),
+            PubspecWarningCode.INVALID_DEPENDENCY, [GIT_FIELD]);
       }
     }
   }
diff --git a/pkg/analyzer/lib/src/pubspec/pubspec_warning_code.dart b/pkg/analyzer/lib/src/pubspec/pubspec_warning_code.dart
index ecc7124..a93cca0 100644
--- a/pkg/analyzer/lib/src/pubspec/pubspec_warning_code.dart
+++ b/pkg/analyzer/lib/src/pubspec/pubspec_warning_code.dart
@@ -52,6 +52,17 @@
       "The value of the 'flutter' field is expected to be a map.",
       correction: "Try converting the value to be a map.");
 
+  /// A code indicating that a versioned package has an invalid dependency (git
+  /// or path).
+  ///
+  /// Parameters:
+  /// 0: the kind of dependency.
+  static const PubspecWarningCode INVALID_DEPENDENCY = PubspecWarningCode(
+      'INVALID_DEPENDENCY', "Publishable packages can't have {0} dependencies.",
+      correction:
+          "Try adding a 'publish_to: none' entry to mark the package as not "
+          "for publishing or remove the {0} dependency.");
+
   /// A code indicating that the name field is missing.
   static const PubspecWarningCode MISSING_NAME = PubspecWarningCode(
       'MISSING_NAME', "The name field is required but missing.",
diff --git a/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart b/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart
index 32f7cf3..8e8944a 100644
--- a/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart
+++ b/pkg/analyzer/test/src/pubspec/pubspec_validator_test.dart
@@ -196,6 +196,73 @@
 ''');
   }
 
+  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('''
@@ -208,6 +275,40 @@
 ''');
   }
 
+  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');
+    newFile('/foo/pubspec.yaml', content: '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
   test_dependencyPath_pubspecDoesNotExist() {
     newFolder('/foo');
     assertErrors('''
@@ -257,6 +358,35 @@
 ''');
   }
 
+  test_dependencyPath_version_error() {
+    newFolder('/foo');
+    newFile('/foo/pubspec.yaml', content: '''
+name: foo
+''');
+    assertErrors('''
+name: sample
+version: 0.1.0
+dependencies:
+  foo:
+    path: /foo
+''', [PubspecWarningCode.INVALID_DEPENDENCY]);
+  }
+
+  test_dependencyPath_version_valid() {
+    newFolder('/foo');
+    newFile('/foo/pubspec.yaml', content: '''
+name: foo
+''');
+    assertNoErrors('''
+name: sample
+version: 0.1.0
+publish_to: none
+dependencies:
+  foo:
+    path: /foo
+''');
+  }
+
   test_dependencyPathDoesNotExist_path_error() {
     assertErrors('''
 name: sample
@@ -288,6 +418,19 @@
 ''');
   }
 
+  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