Allow users to create samples using flutter create. (#23584)

This adds flutter create --sample which allows users to execute a command which will create a working sample app from samples embedded in the API docs.

The command looks something like this:

flutter create --sample=chip.DeletableChipAttributes.onDeleted mysample
diff --git a/dev/snippets/config/skeletons/application.html b/dev/snippets/config/skeletons/application.html
index bbbed4f..479a8c0 100644
--- a/dev/snippets/config/skeletons/application.html
+++ b/dev/snippets/config/skeletons/application.html
@@ -20,7 +20,7 @@
   </div>
   <div class="snippet" id="longSnippet" hidden>
     <div class="snippet-description">To create a sample project with this code snippet, run:<br/>
-      <span class="snippet-create-command">flutter create --snippet={{id}} mysample</span>
+      <span class="snippet-create-command">flutter create --sample={{id}} mysample</span>
     </div>
     <div class="copyable-container">
       <button class="copy-button-overlay copy-button" title="Copy to clipboard"
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index aa6bc7f..27d2d9f 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports
 import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports
@@ -13,6 +14,7 @@
 import '../android/gradle.dart' as gradle;
 import '../base/common.dart';
 import '../base/file_system.dart';
+import '../base/net.dart';
 import '../base/os.dart';
 import '../base/utils.dart';
 import '../cache.dart';
@@ -85,6 +87,20 @@
       defaultsTo: null,
     );
     argParser.addOption(
+      'sample',
+      abbr: 's',
+      help: 'Specifies the Flutter code sample to use as the main.dart for an application. Implies '
+        '--template=app.',
+      defaultsTo: null,
+      valueHelp: 'the sample ID of the desired sample from the API documentation website (http://docs.flutter.io)'
+    );
+    argParser.addFlag(
+      'overwrite',
+      negatable: true,
+      defaultsTo: false,
+      help: 'When performing operations, overwrite existing files.',
+    );
+    argParser.addOption(
       'description',
       defaultsTo: 'A new Flutter project.',
       help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.'
@@ -162,6 +178,19 @@
     return null;
   }
 
+  Future<String> _fetchSampleFromServer(String sampleId) async {
+    // Sanity check the sampleId
+    if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
+      throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the '
+        'documentation and try again.');
+    }
+
+    final String host = FlutterVersion.instance.channel == 'stable'
+        ? 'docs.flutter.io'
+        : 'master-docs-flutter-io.firebaseapp.com';
+    return utf8.decode(await fetchUrl(Uri.https(host, 'snippets/$sampleId.dart')));
+  }
+
   @override
   Future<FlutterCommandResult> runCommand() async {
     if (argResults.rest.isEmpty)
@@ -198,6 +227,17 @@
     final Directory projectDir = fs.directory(argResults.rest.first);
     final String projectDirPath = fs.path.normalize(projectDir.absolute.path);
 
+    String sampleCode;
+    if (argResults['sample'] != null) {
+      if (argResults['template'] != null &&
+        _stringToProjectType(argResults['template'] ?? 'app') != _ProjectType.app) {
+        throwToolExit('Cannot specify --sample with a project type other than '
+          '"${getEnumName(_ProjectType.app)}"');
+      }
+      // Fetch the sample from the server.
+      sampleCode = await _fetchSampleFromServer(argResults['sample']);
+    }
+
     _ProjectType template;
     _ProjectType detectedProjectType;
     final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
@@ -241,7 +281,7 @@
       }
     }
 
-    String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot);
+    String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: argResults['overwrite']);
     if (error != null)
       throwToolExit(error);
 
@@ -262,29 +302,38 @@
     );
 
     final String relativeDirPath = fs.path.relative(projectDirPath);
-    if (!projectDir.existsSync()) {
+    if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
       printStatus('Creating project $relativeDirPath...');
     } else {
+      if (sampleCode != null && !argResults['overwrite']) {
+        throwToolExit('Will not overwrite existing project in $relativeDirPath: '
+          'must specify --overwrite for samples to overwrite.');
+      }
       printStatus('Recreating project $relativeDirPath...');
     }
+
     final Directory relativeDir = fs.directory(projectDirPath);
     int generatedFileCount = 0;
     switch (template) {
       case _ProjectType.app:
-        generatedFileCount += await _generateApp(relativeDir, templateContext);
+        generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: argResults['overwrite']);
         break;
       case _ProjectType.module:
-        generatedFileCount += await _generateModule(relativeDir, templateContext);
+        generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: argResults['overwrite']);
         break;
       case _ProjectType.package:
-        generatedFileCount += await _generatePackage(relativeDir, templateContext);
+        generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: argResults['overwrite']);
         break;
       case _ProjectType.plugin:
-        generatedFileCount += await _generatePlugin(relativeDir, templateContext);
+        generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: argResults['overwrite']);
         break;
     }
+    if (sampleCode != null) {
+      generatedFileCount += await _applySample(relativeDir, sampleCode);
+    }
     printStatus('Wrote $generatedFileCount files.');
     printStatus('\nAll done!');
+    final String application = sampleCode != null ? 'sample application' : 'application';
     if (generatePackage) {
       final String relativeMainPath = fs.path.normalize(fs.path.join(
         relativeDirPath,
@@ -312,12 +361,12 @@
         await doctor.summary();
 
         printStatus('''
-In order to run your application, type:
+In order to run your $application, type:
 
   \$ cd $relativeAppPath
   \$ flutter run
 
-Your application code is in $relativeAppMain.
+Your $application code is in $relativeAppMain.
 ''');
         if (generatePlugin) {
           printStatus('''
@@ -339,20 +388,20 @@
             're-validate your setup.');
         printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
             'directory in order to launch your app.');
-        printStatus('Your application code is in $relativeAppMain');
+        printStatus('Your $application code is in $relativeAppMain');
       }
     }
 
     return null;
   }
 
-  Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext) async {
+  Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
     int generatedCount = 0;
     final String description = argResults.wasParsed('description')
         ? argResults['description']
         : 'A new flutter module project.';
     templateContext['description'] = description;
-    generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext);
+    generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite);
     if (argResults['pub']) {
       await pubGet(
         context: PubContext.create,
@@ -365,13 +414,13 @@
     return generatedCount;
   }
 
-  Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext) async {
+  Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
     int generatedCount = 0;
     final String description = argResults.wasParsed('description')
         ? argResults['description']
         : 'A new Flutter package project.';
     templateContext['description'] = description;
-    generatedCount += _renderTemplate('package', directory, templateContext);
+    generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite);
     if (argResults['pub']) {
       await pubGet(
         context: PubContext.createPackage,
@@ -382,13 +431,13 @@
     return generatedCount;
   }
 
-  Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext) async {
+  Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
     int generatedCount = 0;
     final String description = argResults.wasParsed('description')
         ? argResults['description']
         : 'A new flutter plugin project.';
     templateContext['description'] = description;
-    generatedCount += _renderTemplate('plugin', directory, templateContext);
+    generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
     if (argResults['pub']) {
       await pubGet(
         context: PubContext.createPlugin,
@@ -410,19 +459,19 @@
     templateContext['pluginProjectName'] = projectName;
     templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
 
-    generatedCount += await _generateApp(project.example.directory, templateContext);
+    generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite);
     return generatedCount;
   }
 
-  Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext) async {
+  Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
     int generatedCount = 0;
-    generatedCount += _renderTemplate('app', directory, templateContext);
+    generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite);
     final FlutterProject project = await FlutterProject.fromDirectory(directory);
     generatedCount += _injectGradleWrapper(project);
 
     if (argResults['with-driver-test']) {
       final Directory testDirectory = directory.childDirectory('test_driver');
-      generatedCount += _renderTemplate('driver', testDirectory, templateContext);
+      generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite);
     }
 
     if (argResults['pub']) {
@@ -435,6 +484,20 @@
     return generatedCount;
   }
 
+  // Takes an application template and replaces the main.dart with one from the
+  // documentation website in sampleCode.  Returns the difference in the number
+  // of files after applying the sample, since it also deletes the application's
+  // test directory (since the template's test doesn't apply to the sample).
+  Future<int> _applySample(Directory directory, String sampleCode) async {
+    final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
+    await mainDartFile.create(recursive: true);
+    await mainDartFile.writeAsString(sampleCode);
+    final Directory testDir = directory.childDirectory('test');
+    final List<FileSystemEntity> files = testDir.listSync(recursive: true);
+    await testDir.delete(recursive: true);
+    return -files.length;
+  }
+
   Map<String, dynamic> _templateContext({
     String organization,
     String projectName,
@@ -473,9 +536,9 @@
     };
   }
 
-  int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context) {
+  int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, {bool overwrite = false}) {
     final Template template = Template.fromName(templateName);
-    return template.render(directory, context, overwriteExisting: false);
+    return template.render(directory, context, overwriteExisting: overwrite);
   }
 
   int _injectGradleWrapper(FlutterProject project) {
@@ -557,12 +620,22 @@
 
 /// Return null if the project directory is legal. Return a validation message
 /// if we should disallow the directory name.
-String _validateProjectDir(String dirPath, { String flutterRoot }) {
+String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) {
   if (fs.path.isWithin(flutterRoot, dirPath)) {
     return 'Cannot create a project within the Flutter SDK. '
       "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
   }
 
+  // If the destination directory is actually a file, then we refuse to
+  // overwrite, on the theory that the user probably didn't expect it to exist.
+  if (fs.isFileSync(dirPath)) {
+    return "Invalid project name: '$dirPath' - refers to an existing file."
+        '${overwrite ? ' Refusing to overwrite a file with a directory.' : ''}';
+  }
+
+  if (overwrite)
+    return null;
+
   final FileSystemEntityType type = fs.typeSync(dirPath);
 
   if (type != FileSystemEntityType.notFound) {
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index e3ae88f..1262c57 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -19,7 +19,7 @@
 
 class FlutterVersion {
   @visibleForTesting
-  FlutterVersion(this._clock) {
+  FlutterVersion([this._clock = const Clock()]) {
     _channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}');
     final String branch = _runGit('git rev-parse --abbrev-ref HEAD');
     _branch = branch == 'HEAD' ? _channel : branch;
diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart
index e72171d..c9016f6 100644
--- a/packages/flutter_tools/test/commands/create_test.dart
+++ b/packages/flutter_tools/test/commands/create_test.dart
@@ -8,6 +8,7 @@
 import 'package:args/command_runner.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/net.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/create.dart';
 import 'package:flutter_tools/src/dart/sdk.dart';
@@ -56,6 +57,7 @@
         'ios/Flutter/AppFrameworkInfo.plist',
         'ios/Runner/AppDelegate.m',
         'ios/Runner/GeneratedPluginRegistrant.h',
+        'lib/main.dart',
       ],
     );
     return _runFlutterTest(projectDir);
@@ -719,18 +721,52 @@
     );
   });
 
-  // Verify that we fail with an error code when the file exists.
-  testUsingContext('fails when file exists', () async {
+  testUsingContext('fails when file exists where output directory should be', () async {
     Cache.flutterRoot = '../..';
     final CreateCommand command = CreateCommand();
     final CommandRunner<void> runner = createTestCommandRunner(command);
-    final File existingFile = fs.file('${projectDir.path.toString()}/bad');
+    final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
     if (!existingFile.existsSync()) {
       existingFile.createSync(recursive: true);
     }
     expect(
       runner.run(<String>['create', existingFile.path]),
-      throwsToolExit(message: 'file exists'),
+      throwsToolExit(message: 'existing file'),
+    );
+  });
+
+  testUsingContext('fails overwrite when file exists where output directory should be', () async {
+    Cache.flutterRoot = '../..';
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
+    if (!existingFile.existsSync()) {
+      existingFile.createSync(recursive: true);
+    }
+    expect(
+      runner.run(<String>['create', '--overwrite', existingFile.path]),
+      throwsToolExit(message: 'existing file'),
+    );
+  });
+
+  testUsingContext('overwrites existing directory when requested', () async {
+    Cache.flutterRoot = '../..';
+    final Directory existingDirectory = fs.directory(fs.path.join(projectDir.path, 'bad'));
+    if (!existingDirectory.existsSync()) {
+      existingDirectory.createSync(recursive: true);
+    }
+    final File existingFile = fs.file(fs.path.join(existingDirectory.path, 'lib', 'main.dart'));
+    existingFile.createSync(recursive: true);
+    await _createProject(
+      fs.directory(existingDirectory.path),
+      <String>['--overwrite'],
+      <String>[
+        'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+        'lib/main.dart',
+        'ios/Flutter/AppFrameworkInfo.plist',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/GeneratedPluginRegistrant.h',
+      ],
     );
   });
 
@@ -779,6 +815,24 @@
       ProcessManager: () => loggingProcessManager,
     },
   );
+
+  testUsingContext('can create a sample-based project', () async {
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>['--no-pub', '--sample=foo.bar.Baz'],
+      <String>[
+        'lib/main.dart',
+        'flutter_project.iml',
+        'android/app/src/main/AndroidManifest.xml',
+        'ios/Flutter/AppFrameworkInfo.plist',
+      ],
+      unexpectedPaths: <String>['test'],
+    );
+    expect(projectDir.childDirectory('lib').childFile('main.dart').readAsStringSync(),
+      contains('void main() {}'));
+  }, timeout: allowForRemotePubInvocation, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(200, result: 'void main() {}'),
+  });
 }
 
 Future<void> _createProject(
@@ -901,3 +955,62 @@
     );
   }
 }
+
+class MockHttpClient implements HttpClient {
+  MockHttpClient(this.statusCode, {this.result});
+
+  final int statusCode;
+  final String result;
+
+  @override
+  Future<HttpClientRequest> getUrl(Uri url) async {
+    return MockHttpClientRequest(statusCode, result: result);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClient - $invocation';
+  }
+}
+
+class MockHttpClientRequest implements HttpClientRequest {
+  MockHttpClientRequest(this.statusCode, {this.result});
+
+  final int statusCode;
+  final String result;
+
+  @override
+  Future<HttpClientResponse> close() async {
+    return MockHttpClientResponse(statusCode, result: result);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientRequest - $invocation';
+  }
+}
+
+class MockHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
+  MockHttpClientResponse(this.statusCode, {this.result});
+
+  @override
+  final int statusCode;
+
+  final String result;
+
+  @override
+  String get reasonPhrase => '<reason phrase>';
+
+  @override
+  StreamSubscription<List<int>> listen(void onData(List<int> event), {
+    Function onError, void onDone(), bool cancelOnError
+  }) {
+    return Stream<List<int>>.fromIterable(<List<int>>[result.codeUnits])
+      .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientResponse - $invocation';
+  }
+}