Run command validation on all commands. (#12246)

This makes command validation happen as part of `verifyThenRunCommand()`,
using a newly introduced protected method (`validateCommand()`) rather than
a `commandValidator` property (that subclasses were responsible for manually
invoking).
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index aef95a3..06dd9e4 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -33,21 +33,12 @@
   final String description = 'Flutter build commands.';
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
-  }
-
-  @override
   Future<Null> runCommand() async { }
 }
 
 abstract class BuildSubCommand extends FlutterCommand {
-  @override
-  @mustCallSuper
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
+  BuildSubCommand() {
+    requiresPubspecYaml();
   }
 
   @override
@@ -72,6 +63,10 @@
 }
 
 class BuildCleanCommand extends FlutterCommand {
+  BuildCleanCommand() {
+    requiresPubspecYaml();
+  }
+
   @override
   final String name = 'clean';
 
@@ -79,12 +74,6 @@
   final String description = 'Delete the build/ directory.';
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
-  }
-
-  @override
   Future<Null> runCommand() async {
     final Directory buildDir = fs.directory(getBuildDirectory());
     printStatus("Deleting '${buildDir.path}${fs.path.separator}'.");
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index d465092..53e0704 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -38,6 +38,8 @@
 /// exit code.
 class DriveCommand extends RunCommandBase {
   DriveCommand() {
+    requiresPubspecYaml();
+
     argParser.addFlag(
       'keep-app-running',
       defaultsTo: null,
@@ -90,12 +92,6 @@
   StreamSubscription<String> _deviceLogSubscription;
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
-  }
-
-  @override
   Future<Null> runCommand() async {
     final String testFile = _getTestFile();
     if (testFile == null)
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index 9ae31a9..edc6959 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -12,6 +12,10 @@
 import '../runner/flutter_command.dart';
 
 class InstallCommand extends FlutterCommand {
+  InstallCommand() {
+    requiresPubspecYaml();
+  }
+
   @override
   final String name = 'install';
 
@@ -21,12 +25,11 @@
   Device device;
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
+  Future<Null> validateCommand() async {
+    await super.validateCommand();
     device = await findTargetDevice();
     if (device == null)
       throwToolExit('No target device found');
-    return super.verifyThenRunCommand();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index 6c57df6..00ed183 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -27,17 +27,12 @@
   final String description = 'Commands for managing Flutter packages.';
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
-  }
-
-  @override
   Future<Null> runCommand() async { }
 }
 
 class PackagesGetCommand extends FlutterCommand {
   PackagesGetCommand(this.name, this.upgrade) {
+    requiresPubspecYaml();
     argParser.addFlag('offline',
       negatable: false,
       help: 'Use cached packages instead of accessing the network.'
@@ -84,6 +79,10 @@
 }
 
 class PackagesTestCommand extends FlutterCommand {
+  PackagesTestCommand() {
+    requiresPubspecYaml();
+  }
+
   @override
   String get name => 'test';
 
@@ -107,7 +106,9 @@
 }
 
 class PackagesPassthroughCommand extends FlutterCommand {
-  PackagesPassthroughCommand();
+  PackagesPassthroughCommand() {
+    requiresPubspecYaml();
+  }
 
   @override
   String get name => 'pub';
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 0d7e8d7..6afefa9 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -82,6 +82,8 @@
   final String description = 'Run your Flutter app on an attached device.';
 
   RunCommand({ bool verboseHelp: false }) {
+    requiresPubspecYaml();
+
     argParser.addFlag('full-restart',
         defaultsTo: true,
         help: 'Stop any currently running application process before running the app.');
@@ -153,13 +155,6 @@
             'measure the startup time and the app restart time, write the\n'
             'results out to "refresh_benchmark.json", and exit. This flag is\n'
             'intended for use in generating automated flutter benchmarks.');
-
-    commandValidator = () {
-      // When running with a prebuilt application, no command validation is
-      // necessary.
-      if (!runningWithPrebuiltApplication)
-        commonCommandValidator();
-    };
   }
 
   List<Device> devices;
@@ -222,14 +217,16 @@
   bool get stayResident => argResults['resident'];
 
   @override
-  Future<FlutterCommandResult> verifyThenRunCommand() async {
-    commandValidator();
+  Future<Null> validateCommand() async {
+    // When running with a prebuilt application, no command validation is
+    // necessary.
+    if (!runningWithPrebuiltApplication)
+      await super.validateCommand();
     devices = await findAllTargetDevices();
     if (devices == null)
       throwToolExit(null);
     if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication)
       throwToolExit('Using -d all with --use-application-binary is not supported');
-    return super.verifyThenRunCommand();
   }
 
   DebuggingOptions _createDebuggingOptions() {
diff --git a/packages/flutter_tools/lib/src/commands/stop.dart b/packages/flutter_tools/lib/src/commands/stop.dart
index 6cc4138..089c13d 100644
--- a/packages/flutter_tools/lib/src/commands/stop.dart
+++ b/packages/flutter_tools/lib/src/commands/stop.dart
@@ -12,6 +12,10 @@
 import '../runner/flutter_command.dart';
 
 class StopCommand extends FlutterCommand {
+  StopCommand() {
+    requiresPubspecYaml();
+  }
+
   @override
   final String name = 'stop';
 
@@ -21,12 +25,11 @@
   Device device;
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
+  Future<Null> validateCommand() async {
+    await super.validateCommand();
     device = await findTargetDevice();
     if (device == null)
       throwToolExit(null);
-    return super.verifyThenRunCommand();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 1af793b..4750b2f 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -21,6 +21,7 @@
 
 class TestCommand extends FlutterCommand {
   TestCommand({ bool verboseHelp: false }) {
+    requiresPubspecYaml();
     usesPubOption();
     argParser.addOption('name',
       help: 'A regular expression matching substrings of the names of tests to run.',
@@ -67,15 +68,6 @@
         negatable: false,
         help: 'Handle machine structured JSON command input\n'
             'and provide output and progress in machine friendly format.');
-    commandValidator = () {
-      if (!fs.isFileSync('pubspec.yaml')) {
-        throwToolExit(
-          'Error: No pubspec.yaml file found in the current working directory.\n'
-          'Run this command from the root of your project. Test files must be\n'
-          'called *_test.dart and must reside in the package\'s \'test\'\n'
-          'directory (or one of its subdirectories).');
-      }
-    };
   }
 
   @override
@@ -144,6 +136,18 @@
   }
 
   @override
+  Future<Null> validateCommand() async {
+    await super.validateCommand();
+    if (!fs.isFileSync('pubspec.yaml')) {
+      throwToolExit(
+          'Error: No pubspec.yaml file found in the current working directory.\n'
+              'Run this command from the root of your project. Test files must be\n'
+              'called *_test.dart and must reside in the package\'s \'test\'\n'
+              'directory (or one of its subdirectories).');
+    }
+  }
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     if (platform.isWindows) {
       throwToolExit(
@@ -152,7 +156,6 @@
       );
     }
 
-    commandValidator();
     final List<String> names = argResults['name'];
     final List<String> plainNames = argResults['plain-name'];
 
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index aa80b2b..4674d67 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -21,6 +21,7 @@
 
 class TraceCommand extends FlutterCommand {
   TraceCommand() {
+    requiresPubspecYaml();
     argParser.addFlag('start', negatable: false, help: 'Start tracing.');
     argParser.addFlag('stop', negatable: false, help: 'Stop tracing.');
     argParser.addOption('out', help: 'Specify the path of the saved trace file.');
@@ -44,12 +45,6 @@
     'with --start and later with --stop.';
 
   @override
-  Future<Null> verifyThenRunCommand() async {
-    commandValidator();
-    return super.verifyThenRunCommand();
-  }
-
-  @override
   Future<Null> runCommand() async {
     final int observatoryPort = int.parse(argResults['debug-port']);
 
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 8a2d30f..bd55693 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -22,8 +22,6 @@
 import '../usage.dart';
 import 'flutter_command_runner.dart';
 
-typedef void Validator();
-
 enum ExitStatus {
   success,
   warning,
@@ -57,13 +55,11 @@
 }
 
 abstract class FlutterCommand extends Command<Null> {
-  FlutterCommand() {
-    commandValidator = commonCommandValidator;
-  }
-
   @override
   FlutterCommandRunner get runner => super.runner;
 
+  bool _requiresPubspecYaml = false;
+
   /// Whether this command uses the 'target' option.
   bool _usesTargetOption = false;
 
@@ -75,6 +71,10 @@
 
   BuildMode _defaultBuildMode;
 
+  void requiresPubspecYaml() {
+    _requiresPubspecYaml = true;
+  }
+
   void usesTargetOption() {
     argParser.addOption('target',
       abbr: 't',
@@ -219,6 +219,8 @@
   /// rather than calling [runCommand] directly.
   @mustCallSuper
   Future<FlutterCommandResult> verifyThenRunCommand() async {
+    await validateCommand();
+
     // Populate the cache. We call this before pub get below so that the sky_engine
     // package is available in the flutter cache for pub to find.
     if (shouldUpdateCache)
@@ -313,11 +315,10 @@
     printStatus('No connected devices.');
   }
 
-  // This is a field so that you can modify the value for testing.
-  Validator commandValidator;
-
-  void commonCommandValidator() {
-    if (!PackageMap.isUsingCustomPackagesPath) {
+  @protected
+  @mustCallSuper
+  Future<Null> validateCommand() async {
+    if (_requiresPubspecYaml && !PackageMap.isUsingCustomPackagesPath) {
       // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
       if (!fs.isFileSync('pubspec.yaml')) {
         throw new ToolExit(
@@ -326,6 +327,7 @@
           'Do not run this command from the root of your git clone of Flutter.'
         );
       }
+
       if (fs.isFileSync('flutter.yaml')) {
         throw new ToolExit(
           'Please merge your flutter.yaml into your pubspec.yaml.\n\n'
@@ -343,6 +345,13 @@
           'https://github.com/flutter/flutter/blob/master/examples/flutter_gallery/pubspec.yaml\n'
         );
       }
+
+      // Validate the current package map only if we will not be running "pub get" later.
+      if (!(_usesPubOption && argResults['pub'])) {
+        final String error = new PackageMap(PackageMap.globalPackagesPath).checkValid();
+        if (error != null)
+          throw new ToolExit(error);
+      }
     }
 
     if (_usesTargetOption) {
@@ -350,13 +359,6 @@
       if (!fs.isFileSync(targetPath))
         throw new ToolExit('Target file "$targetPath" not found.');
     }
-
-    // Validate the current package map only if we will not be running "pub get" later.
-    if (!(_usesPubOption && argResults['pub'])) {
-      final String error = new PackageMap(PackageMap.globalPackagesPath).checkValid();
-      if (error != null)
-        throw new ToolExit(error);
-    }
   }
 
   ApplicationPackageStore applicationPackages;
diff --git a/packages/flutter_tools/test/commands/drive_test.dart b/packages/flutter_tools/test/commands/drive_test.dart
index aac7695..b6d0e8b 100644
--- a/packages/flutter_tools/test/commands/drive_test.dart
+++ b/packages/flutter_tools/test/commands/drive_test.dart
@@ -76,10 +76,11 @@
 
       final String testApp = fs.path.join(cwd.path, 'test', 'e2e.dart');
       final String testFile = fs.path.join(cwd.path, 'test_driver', 'e2e_test.dart');
+      fs.file(testApp).createSync(recursive: true);
 
       final List<String> args = <String>[
         'drive',
-        '--target=$testApp}',
+        '--target=$testApp',
       ];
       try {
         await createTestCommandRunner(command).run(args);
@@ -120,6 +121,7 @@
 
     testUsingContext('returns 1 when app file is outside package', () async {
       final String appFile = fs.path.join(cwd.dirname, 'other_app', 'app.dart');
+      fs.file(appFile).createSync(recursive: true);
       final List<String> args = <String>[
         'drive',
         '--target=$appFile',
@@ -139,6 +141,7 @@
 
     testUsingContext('returns 1 when app file is in the root dir', () async {
       final String appFile = fs.path.join(cwd.path, 'main.dart');
+      fs.file(appFile).createSync(recursive: true);
       final List<String> args = <String>[
         'drive',
         '--target=$appFile',
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 84dcbeb..acf5352 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -103,8 +103,7 @@
 
 void applyMocksToCommand(FlutterCommand command) {
   command
-    ..applicationPackages = new MockApplicationPackageStore()
-    ..commandValidator = () => true;
+    ..applicationPackages = new MockApplicationPackageStore();
 }
 
 /// Common functionality for tracking mock interaction