analyze.dart 59.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
6 7
import 'dart:core' hide print;
import 'dart:io' hide exit;
8
import 'dart:typed_data';
9

10
import 'package:crypto/crypto.dart';
11
import 'package:meta/meta.dart';
12
import 'package:path/path.dart' as path;
13 14

import 'run_command.dart';
15
import 'utils.dart';
16 17 18 19 20 21 22 23 24 25 26 27 28

final String flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
final String flutter = path.join(flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
final String dart = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
final String pub = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
final String pubCache = path.join(flutterRoot, '.pub-cache');

/// When you call this, you can pass additional arguments to pass custom
/// arguments to flutter analyze. For example, you might want to call this
/// script with the parameter --dart-sdk to use custom dart sdk.
///
/// For example:
/// bin/cache/dart-sdk/bin/dart dev/bots/analyze.dart --dart-sdk=/tmp/dart-sdk
29
Future<void> main(List<String> arguments) async {
Ian Hickson's avatar
Ian Hickson committed
30
  print('$clock STARTING ANALYSIS');
31 32 33 34 35
  try {
    await run(arguments);
  } on ExitException catch (error) {
    error.apply();
  }
36
  print('$clock ${bold}Analysis successful.$reset');
37 38 39
}

Future<void> run(List<String> arguments) async {
40 41 42
  bool assertsEnabled = false;
  assert(() { assertsEnabled = true; return true; }());
  if (!assertsEnabled) {
43
    exitWithError(<String>['The analyze.dart script must be run with --enable-asserts.']);
44
  }
45

46 47 48
  print('$clock runtimeType in toString...');
  await verifyNoRuntimeTypeInToString(flutterRoot);

49 50 51 52 53 54
  print('$clock Unexpected binaries...');
  await verifyNoBinaries(flutterRoot);

  print('$clock Trailing spaces...');
  await verifyNoTrailingSpaces(flutterRoot); // assumes no unexpected binaries, so should be after verifyNoBinaries

Ian Hickson's avatar
Ian Hickson committed
55
  print('$clock Deprecations...');
56
  await verifyDeprecations(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
57 58

  print('$clock Licenses...');
59
  await verifyNoMissingLicense(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
60 61

  print('$clock Test imports...');
62
  await verifyNoTestImports(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
63 64

  print('$clock Test package imports...');
65
  await verifyNoTestPackageImports(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
66 67

  print('$clock Bad imports (framework)...');
68
  await verifyNoBadImportsInFlutter(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
69 70

  print('$clock Bad imports (tools)...');
71
  await verifyNoBadImportsInFlutterTools(flutterRoot);
Ian Hickson's avatar
Ian Hickson committed
72 73

  print('$clock Internationalization...');
74
  await verifyInternationalizations();
Ian Hickson's avatar
Ian Hickson committed
75

76
  // Ensure that all package dependencies are in sync.
Ian Hickson's avatar
Ian Hickson committed
77
  print('$clock Package dependencies...');
78 79 80 81
  await runCommand(flutter, <String>['update-packages', '--verify-only'],
    workingDirectory: flutterRoot,
  );

82
  // Analyze all the Dart code in the repo.
Ian Hickson's avatar
Ian Hickson committed
83
  print('$clock Dart analysis...');
84 85 86 87 88
  await _runFlutterAnalyze(flutterRoot, options: <String>[
    '--flutter-repo',
    ...arguments,
  ]);

89 90
  // Try with the --watch analyzer, to make sure it returns success also.
  // The --benchmark argument exits after one run.
Ian Hickson's avatar
Ian Hickson committed
91
  print('$clock Dart analysis (with --watch)...');
92 93 94 95 96 97
  await _runFlutterAnalyze(flutterRoot, options: <String>[
    '--flutter-repo',
    '--watch',
    '--benchmark',
    ...arguments,
  ]);
98

99 100 101 102 103 104 105
  // Analyze all the sample code in the repo
  print('$clock Sample code...');
  await runCommand(dart,
    <String>[path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')],
    workingDirectory: flutterRoot,
  );

106
  // Try analysis against a big version of the gallery; generate into a temporary directory.
Ian Hickson's avatar
Ian Hickson committed
107
  print('$clock Dart analysis (mega gallery)...');
108 109 110 111 112 113 114 115 116 117
  final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.');
  try {
    await runCommand(dart,
      <String>[
        path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'),
        '--out',
        outDir.path,
      ],
      workingDirectory: flutterRoot,
    );
Ian Hickson's avatar
Ian Hickson committed
118 119 120 121 122
    await _runFlutterAnalyze(outDir.path, options: <String>[
      '--watch',
      '--benchmark',
      ...arguments,
    ]);
123 124 125 126 127
  } finally {
    outDir.deleteSync(recursive: true);
  }
}

128 129 130 131 132 133

// TESTS

final RegExp _findDeprecationPattern = RegExp(r'@[Dd]eprecated');
final RegExp _deprecationPattern1 = RegExp(r'^( *)@Deprecated\($'); // ignore: flutter_deprecation_syntax (see analyze.dart)
final RegExp _deprecationPattern2 = RegExp(r"^ *'(.+) '$");
134
final RegExp _deprecationPattern3 = RegExp(r"^ *'This feature was deprecated after v([0-9]+)\.([0-9]+)\.([0-9]+)(\-[0-9]+\.[0-9]+\.pre)?\.'$");
135 136 137 138 139 140 141 142 143
final RegExp _deprecationPattern4 = RegExp(r'^ *\)$');

/// Some deprecation notices are special, for example they're used to annotate members that
/// will never go away and were never allowed but which we are trying to show messages for.
/// (One example would be a library that intentionally conflicts with a member in another
/// library to indicate that it is incompatible with that other library. Another would be
/// the regexp just above...)
const String _ignoreDeprecation = ' // ignore: flutter_deprecation_syntax (see analyze.dart)';

144 145
/// Some deprecation notices are exempt for historical reasons. They must have an issue listed.
final RegExp _legacyDeprecation = RegExp(r' // ignore: flutter_deprecation_syntax, https://github.com/flutter/flutter/issues/[0-9]+$');
146

147
Future<void> verifyDeprecations(String workingDirectory, { int minimumMatches = 2000 }) async {
148
  final List<String> errors = <String>[];
149
  await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
150 151 152
    int lineNumber = 0;
    final List<String> lines = file.readAsLinesSync();
    final List<int> linesWithDeprecations = <int>[];
153
    for (final String line in lines) {
154 155
      if (line.contains(_findDeprecationPattern) &&
          !line.endsWith(_ignoreDeprecation) &&
156
          !line.contains(_legacyDeprecation)) {
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
        linesWithDeprecations.add(lineNumber);
      }
      lineNumber += 1;
    }
    for (int lineNumber in linesWithDeprecations) {
      try {
        final Match match1 = _deprecationPattern1.firstMatch(lines[lineNumber]);
        if (match1 == null)
          throw 'Deprecation notice does not match required pattern.';
        final String indent = match1[1];
        lineNumber += 1;
        if (lineNumber >= lines.length)
          throw 'Incomplete deprecation notice.';
        Match match3;
        String message;
        do {
          final Match match2 = _deprecationPattern2.firstMatch(lines[lineNumber]);
          if (match2 == null)
            throw 'Deprecation notice does not match required pattern.';
          if (!lines[lineNumber].startsWith("$indent  '"))
            throw 'Unexpected deprecation notice indent.';
          if (message == null) {
            final String firstChar = String.fromCharCode(match2[1].runes.first);
            if (firstChar.toUpperCase() != firstChar)
181
              throw 'Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo';
182 183 184 185 186 187 188
          }
          message = match2[1];
          lineNumber += 1;
          if (lineNumber >= lines.length)
            throw 'Incomplete deprecation notice.';
          match3 = _deprecationPattern3.firstMatch(lines[lineNumber]);
        } while (match3 == null);
189 190 191 192 193 194 195
        final int v1 = int.parse(match3[1]);
        final int v2 = int.parse(match3[2]);
        final bool hasV4 = match3[4] != null;
        if (v1 > 1 || (v1 == 1 && v2 >= 20)) {
          if (!hasV4)
            throw 'Deprecation notice does not accurately indicate a dev branch version number; please see https://flutter.dev/docs/development/tools/sdk/releases to find the latest dev build version number.';
        }
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
        if (!message.endsWith('.') && !message.endsWith('!') && !message.endsWith('?'))
          throw 'Deprecation notice should be a grammatically correct sentence and end with a period.';
        if (!lines[lineNumber].startsWith("$indent  '"))
          throw 'Unexpected deprecation notice indent.';
        lineNumber += 1;
        if (lineNumber >= lines.length)
          throw 'Incomplete deprecation notice.';
        if (!lines[lineNumber].contains(_deprecationPattern4))
          throw 'End of deprecation notice does not match required pattern.';
        if (!lines[lineNumber].startsWith('$indent)'))
          throw 'Unexpected deprecation notice indent.';
      } catch (error) {
        errors.add('${file.path}:${lineNumber + 1}: $error');
      }
    }
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
214 215 216 217
    exitWithError(<String>[
      ...errors,
      '${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes$reset',
    ]);
218 219 220
  }
}

Ian Hickson's avatar
Ian Hickson committed
221 222 223 224 225 226 227
String _generateLicense(String prefix) {
  assert(prefix != null);
  return '${prefix}Copyright 2014 The Flutter Authors. All rights reserved.\n'
         '${prefix}Use of this source code is governed by a BSD-style license that can be\n'
         '${prefix}found in the LICENSE file.';
}

228 229 230
Future<void> verifyNoMissingLicense(String workingDirectory, { bool checkMinimums = true }) async {
  final int overrideMinimumMatches = checkMinimums ? null : 0;
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'dart', overrideMinimumMatches ?? 2000, _generateLicense('// '));
231
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'java', overrideMinimumMatches ?? 39, _generateLicense('// '));
232 233 234 235 236 237 238 239 240 241
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'h', overrideMinimumMatches ?? 30, _generateLicense('// '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'm', overrideMinimumMatches ?? 30, _generateLicense('// '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'swift', overrideMinimumMatches ?? 10, _generateLicense('// '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'gradle', overrideMinimumMatches ?? 100, _generateLicense('// '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'gn', overrideMinimumMatches ?? 0, _generateLicense('# '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'sh', overrideMinimumMatches ?? 1, '#!/usr/bin/env bash\n' + _generateLicense('# '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'bat', overrideMinimumMatches ?? 1, '@ECHO off\n' + _generateLicense('REM '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'ps1', overrideMinimumMatches ?? 1, _generateLicense('# '));
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'html', overrideMinimumMatches ?? 1, '<!DOCTYPE HTML>\n<!-- ${_generateLicense('')} -->', trailingBlank: false);
  await _verifyNoMissingLicenseForExtension(workingDirectory, 'xml', overrideMinimumMatches ?? 1, '<!-- ${_generateLicense('')} -->');
Ian Hickson's avatar
Ian Hickson committed
242 243
}

244
Future<void> _verifyNoMissingLicenseForExtension(String workingDirectory, String extension, int minimumMatches, String license, { bool trailingBlank = true }) async {
Ian Hickson's avatar
Ian Hickson committed
245 246
  assert(!license.endsWith('\n'));
  final String licensePattern = license + '\n' + (trailingBlank ? '\n' : '');
247
  final List<String> errors = <String>[];
248
  await for (final File file in _allFiles(workingDirectory, extension, minimumMatches: minimumMatches)) {
Ian Hickson's avatar
Ian Hickson committed
249 250 251 252
    final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
    if (contents.isEmpty)
      continue; // let's not go down the /bin/true rabbit hole
    if (!contents.startsWith(licensePattern))
253
      errors.add(file.path);
254
  }
255 256
  // Fail if any errors
  if (errors.isNotEmpty) {
Ian Hickson's avatar
Ian Hickson committed
257
    final String s = errors.length == 1 ? ' does' : 's do';
258 259 260 261 262 263 264
    exitWithError(<String>[
      '${bold}The following ${errors.length} file$s not have the right license header:$reset',
      ...errors,
      'The expected license header is:',
      license,
      if (trailingBlank) '...followed by a blank line.',
    ]);
265 266 267
  }
}

268 269 270 271 272
final RegExp _testImportPattern = RegExp(r'''import (['"])([^'"]+_test\.dart)\1''');
const Set<String> _exemptTestImports = <String>{
  'package:flutter_test/flutter_test.dart',
  'hit_test.dart',
  'package:test_api/src/backend/live_test.dart',
273
  'package:integration_test/integration_test.dart',
274
};
275

276 277 278
Future<void> verifyNoTestImports(String workingDirectory) async {
  final List<String> errors = <String>[];
  assert("// foo\nimport 'binding_test.dart' as binding;\n'".contains(_testImportPattern));
279
  final List<File> dartFiles = await _allFiles(path.join(workingDirectory, 'packages'), 'dart', minimumMatches: 1500).toList();
280 281
  for (final File file in dartFiles) {
    for (final String line in file.readAsLinesSync()) {
282 283 284
      final Match match = _testImportPattern.firstMatch(line);
      if (match != null && !_exemptTestImports.contains(match.group(2)))
        errors.add(file.path);
285 286
    }
  }
287 288 289
  // Fail if any errors
  if (errors.isNotEmpty) {
    final String s = errors.length == 1 ? '' : 's';
290 291 292 293
    exitWithError(<String>[
      '${bold}The following file$s import a test directly. Test utilities should be in their own file.$reset',
      ...errors,
    ]);
294 295 296
  }
}

297
Future<void> verifyNoTestPackageImports(String workingDirectory) async {
298 299
  // TODO(ianh): Remove this whole test once https://github.com/dart-lang/matcher/issues/98 is fixed.
  final List<String> shims = <String>[];
300
  final List<String> errors = (await _allFiles(workingDirectory, 'dart', minimumMatches: 2000).toList())
301
    .map<String>((File file) {
302 303
      final String name = Uri.file(path.relative(file.path,
          from: workingDirectory)).toFilePath(windows: false);
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
      if (name.startsWith('bin/cache') ||
          name == 'dev/bots/test.dart' ||
          name.startsWith('.pub-cache'))
        return null;
      final String data = file.readAsStringSync();
      if (data.contains("import 'package:test/test.dart'")) {
        if (data.contains("// Defines a 'package:test' shim.")) {
          shims.add('  $name');
          if (!data.contains('https://github.com/dart-lang/matcher/issues/98'))
            return '  $name: Shims must link to the isInstanceOf issue.';
          if (data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;") &&
              data.contains("export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;"))
            return null;
          return '  $name: Shim seems to be missing the expected import/export lines.';
        }
        final int count = 'package:test'.allMatches(data).length;
320
        if (path.split(file.path).contains('test_driver') ||
321 322
            name.startsWith('dev/missing_dependency_tests/') ||
            name.startsWith('dev/automated_tests/') ||
323
            name.startsWith('dev/snippets/') ||
324 325 326
            name.startsWith('packages/flutter/test/engine/') ||
            name.startsWith('examples/layers/test/smoketests/raw/') ||
            name.startsWith('examples/layers/test/smoketests/rendering/') ||
327
            name.startsWith('dev/integration_tests/flutter_gallery/test/calculator')) {
328 329 330 331 332 333 334 335 336
          // We only exempt driver tests, some of our special trivial tests.
          // Driver tests aren't typically expected to use TypeMatcher and company.
          // The trivial tests don't typically do anything at all and it would be
          // a pain to have to give them a shim.
          if (!data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;"))
            return '  $name: test does not hide TypeMatcher and isInstanceOf from package:test; consider using a shim instead.';
          assert(count > 0);
          if (count == 1)
            return null;
337
          return "  $name: uses 'package:test' $count times.";
338 339 340 341 342 343 344 345 346 347
        }
        if (name.startsWith('packages/flutter_test/')) {
          // flutter_test has deep ties to package:test
          return null;
        }
        if (data.contains("import 'package:test/test.dart' as test_package;") ||
            data.contains("import 'package:test/test.dart' as test_package show ")) {
          if (count == 1)
            return null;
        }
348
        return "  $name: uses 'package:test' directly";
349 350 351 352 353 354 355 356 357 358 359
      }
      return null;
    })
    .where((String line) => line != null)
    .toList()
    ..sort();

  // Fail if any errors
  if (errors.isNotEmpty) {
    final String s1 = errors.length == 1 ? 's' : '';
    final String s2 = errors.length == 1 ? '' : 's';
360
    exitWithError(<String>[
361
      "${bold}The following file$s2 use$s1 'package:test' incorrectly:$reset",
362
      ...errors,
363
      "Rather than depending on 'package:test' directly, use one of the shims:",
364
      ...shims,
365
      "This insulates us from breaking changes in 'package:test'."
366
    ]);
367 368 369
  }
}

370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async {
  final List<String> errors = <String>[];
  final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib');
  final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src');
  // Verify there's one libPath/*.dart for each srcPath/*/.
  final List<String> packages = Directory(libPath).listSync()
    .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')
    .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path))
    .toList()..sort();
  final List<String> directories = Directory(srcPath).listSync()
    .whereType<Directory>()
    .map<String>((Directory entity) => path.basename(entity.path))
    .toList()..sort();
  if (!_listEquals<String>(packages, directories)) {
    errors.add(
      'flutter/lib/*.dart does not match flutter/lib/src/*/:\n'
      'These are the exported packages:\n' +
      packages.map<String>((String path) => '  lib/$path.dart').join('\n') +
      'These are the directories:\n' +
      directories.map<String>((String path) => '  lib/src/$path/').join('\n')
    );
  }
  // Verify that the imports are well-ordered.
  final Map<String, Set<String>> dependencyMap = <String, Set<String>>{};
394
  for (final String directory in directories) {
395
    dependencyMap[directory] = await _findFlutterDependencies(path.join(srcPath, directory), errors, checkForMeta: directory != 'foundation');
396 397 398 399
  }
  assert(dependencyMap['material'].contains('widgets') &&
         dependencyMap['widgets'].contains('rendering') &&
         dependencyMap['rendering'].contains('painting')); // to make sure we're convinced _findFlutterDependencies is finding some
400
  for (final String package in dependencyMap.keys) {
401 402 403 404 405 406
    if (dependencyMap[package].contains(package)) {
      errors.add(
        'One of the files in the $yellow$package$reset package imports that package recursively.'
      );
    }
  }
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422

  for (final String key in dependencyMap.keys) {
    for (final String dependency in dependencyMap[key]) {
      if (dependencyMap[dependency] != null)
        continue;
      // Sanity check before performing _deepSearch, to ensure there's no rogue
      // dependencies.
      final String validFilenames = dependencyMap.keys.map((String name) => name + '.dart').join(', ');
      errors.add(
        '$key imported package:flutter/$dependency.dart '
        'which is not one of the valid exports { $validFilenames }.\n'
        'Consider changing $dependency.dart to one of them.'
      );
    }
  }

423
  for (final String package in dependencyMap.keys) {
424
    final List<String> loop = _deepSearch<String>(dependencyMap, package);
425 426 427 428 429 430 431 432 433
    if (loop != null) {
      errors.add(
        '${yellow}Dependency loop:$reset ' +
        loop.join(' depends on ')
      );
    }
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
434 435 436 437 438 439 440
    exitWithError(<String>[
      if (errors.length == 1)
        '${bold}An error was detected when looking at import dependencies within the Flutter package:$reset'
      else
        '${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset',
      ...errors,
    ]);
441 442 443
  }
}

444 445
Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async {
  final List<String> errors = <String>[];
446
  final List<File> files = await _allFiles(path.join(workingDirectory, 'packages', 'flutter_tools', 'lib'), 'dart', minimumMatches: 200).toList();
447
  for (final File file in files) {
448 449 450 451 452 453
    if (file.readAsStringSync().contains('package:flutter_tools/')) {
      errors.add('$yellow${file.path}$reset imports flutter_tools.');
    }
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
454 455 456 457 458 459 460
    exitWithError(<String>[
      if (errors.length == 1)
        '${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset'
      else
        '${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset',
      ...errors.map((String paragraph) => '$paragraph\n'),
    ]);
461 462 463 464 465 466 467
  }
}

Future<void> verifyInternationalizations() async {
  final EvalResult materialGenResult = await _evalCommand(
    dart,
    <String>[
468
      path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
469 470 471 472 473 474 475
      '--material',
    ],
    workingDirectory: flutterRoot,
  );
  final EvalResult cupertinoGenResult = await _evalCommand(
    dart,
    <String>[
476
      path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
477 478 479 480 481 482 483 484 485 486 487
      '--cupertino',
    ],
    workingDirectory: flutterRoot,
  );

  final String materialLocalizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_material_localizations.dart');
  final String cupertinoLocalizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_cupertino_localizations.dart');
  final String expectedMaterialResult = await File(materialLocalizationsFile).readAsString();
  final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString();

  if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) {
488 489 490 491 492 493 494 495 496 497
    exitWithError(<String>[
      '<<<<<<< $materialLocalizationsFile',
      expectedMaterialResult.trim(),
      '=======',
      materialGenResult.stdout.trim(),
      '>>>>>>> gen_localizations',
      'The contents of $materialLocalizationsFile are different from that produced by gen_localizations.',
      '',
      'Did you forget to run gen_localizations.dart after updating a .arb file?',
    ]);
498 499
  }
  if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) {
500 501 502 503 504 505 506 507 508 509
    exitWithError(<String>[
      '<<<<<<< $cupertinoLocalizationsFile',
      expectedCupertinoResult.trim(),
      '=======',
      cupertinoGenResult.stdout.trim(),
      '>>>>>>> gen_localizations',
      'The contents of $cupertinoLocalizationsFile are different from that produced by gen_localizations.',
      '',
      'Did you forget to run gen_localizations.dart after updating a .arb file?',
    ]);
510 511 512
  }
}

513 514 515 516 517
Future<void> verifyNoRuntimeTypeInToString(String workingDirectory) async {
  final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
  final Set<String> excludedFiles = <String>{
    path.join(flutterLib, 'src', 'foundation', 'object.dart'), // Calls this from within an assert.
  };
518
  final List<File> files = await _allFiles(flutterLib, 'dart', minimumMatches: 400)
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
      .where((File file) => !excludedFiles.contains(file.path))
      .toList();
  final RegExp toStringRegExp = RegExp(r'^\s+String\s+to(.+?)?String(.+?)?\(\)\s+(\{|=>)');
  final List<String> problems = <String>[];
  for (final File file in files) {
    final List<String> lines = file.readAsLinesSync();
    for (int index = 0; index < lines.length; index++) {
      if (toStringRegExp.hasMatch(lines[index])) {
        final int sourceLine = index + 1;
        bool _checkForRuntimeType(String line) {
          if (line.contains(r'$runtimeType') || line.contains('runtimeType.toString()')) {
            problems.add('${file.path}:$sourceLine}: toString calls runtimeType.toString');
            return true;
          }
          return false;
        }
        if (_checkForRuntimeType(lines[index])) {
          continue;
        }
        if (lines[index].contains('=>')) {
          while (!lines[index].contains(';')) {
            index++;
            assert(index < lines.length, 'Source file $file has unterminated toString method.');
            if (_checkForRuntimeType(lines[index])) {
              break;
            }
          }
        } else {
          int openBraceCount = '{'.allMatches(lines[index]).length - '}'.allMatches(lines[index]).length;
          while (!lines[index].contains('}') && openBraceCount > 0) {
            index++;
            assert(index < lines.length, 'Source file $file has unbalanced braces in a toString method.');
            if (_checkForRuntimeType(lines[index])) {
              break;
            }
            openBraceCount += '{'.allMatches(lines[index]).length;
            openBraceCount -= '}'.allMatches(lines[index]).length;
          }
        }
      }
    }
  }
  if (problems.isNotEmpty)
    exitWithError(problems);
}

565
Future<void> verifyNoTrailingSpaces(String workingDirectory, { int minimumMatches = 4000 }) async {
566
  final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches)
567 568 569 570 571
    .where((File file) => path.basename(file.path) != 'serviceaccount.enc')
    .where((File file) => path.basename(file.path) != 'Ahem.ttf')
    .where((File file) => path.extension(file.path) != '.snapshot')
    .where((File file) => path.extension(file.path) != '.png')
    .where((File file) => path.extension(file.path) != '.jpg')
572
    .where((File file) => path.extension(file.path) != '.ico')
573
    .where((File file) => path.extension(file.path) != '.jar')
574
    .where((File file) => path.extension(file.path) != '.swp')
575 576
    .toList();
  final List<String> problems = <String>[];
577
  for (final File file in files) {
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
    final List<String> lines = file.readAsLinesSync();
    for (int index = 0; index < lines.length; index += 1) {
      if (lines[index].endsWith(' ')) {
        problems.add('${file.path}:${index + 1}: trailing U+0020 space character');
      } else if (lines[index].endsWith('\t')) {
        problems.add('${file.path}:${index + 1}: trailing U+0009 tab character');
      }
    }
    if (lines.isNotEmpty && lines.last == '')
      problems.add('${file.path}:${lines.length}: trailing blank line');
  }
  if (problems.isNotEmpty)
    exitWithError(problems);
}

593
@immutable
594
class Hash256 {
595
  const Hash256(this.a, this.b, this.c, this.d);
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656

  factory Hash256.fromDigest(Digest digest) {
    assert(digest.bytes.length == 32);
    return Hash256(
      digest.bytes[ 0] << 56 |
      digest.bytes[ 1] << 48 |
      digest.bytes[ 2] << 40 |
      digest.bytes[ 3] << 32 |
      digest.bytes[ 4] << 24 |
      digest.bytes[ 5] << 16 |
      digest.bytes[ 6] <<  8 |
      digest.bytes[ 7] <<  0,
      digest.bytes[ 8] << 56 |
      digest.bytes[ 9] << 48 |
      digest.bytes[10] << 40 |
      digest.bytes[11] << 32 |
      digest.bytes[12] << 24 |
      digest.bytes[13] << 16 |
      digest.bytes[14] <<  8 |
      digest.bytes[15] <<  0,
      digest.bytes[16] << 56 |
      digest.bytes[17] << 48 |
      digest.bytes[18] << 40 |
      digest.bytes[19] << 32 |
      digest.bytes[20] << 24 |
      digest.bytes[21] << 16 |
      digest.bytes[22] <<  8 |
      digest.bytes[23] <<  0,
      digest.bytes[24] << 56 |
      digest.bytes[25] << 48 |
      digest.bytes[26] << 40 |
      digest.bytes[27] << 32 |
      digest.bytes[28] << 24 |
      digest.bytes[29] << 16 |
      digest.bytes[30] <<  8 |
      digest.bytes[31] <<  0,
    );
  }

  final int a;
  final int b;
  final int c;
  final int d;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is Hash256
        && other.a == a
        && other.b == b
        && other.c == c
        && other.d == d;
  }

  @override
  int get hashCode => a ^ b ^ c ^ d;
}

// DO NOT ADD ANY ENTRIES TO THIS LIST.
// We have a policy of not checking in binaries into this repository.
657 658 659
// If you are adding/changing template images, use the flutter_template_images
// package and a .img.tmpl placeholder instead.
// If you have other binaries to add, please consult Hixie for advice.
660
final Set<Hash256> _legacyBinaries = <Hash256>{
661 662 663 664 665
  // DEFAULT ICON IMAGES

  // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-hdpi/ic_launcher.png
  // packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png
  // (also used by many examples)
666
  const Hash256(0x6A7C8F0D703E3682, 0x108F9662F8133022, 0x36240D3F8F638BB3, 0x91E32BFB96055FEF),
667 668 669

  // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-mdpi/ic_launcher.png
  // (also used by many examples)
670
  const Hash256(0xC7C0C0189145E4E3, 0x2A401C61C9BDC615, 0x754B0264E7AFAE24, 0xE834BB81049EAF81),
671 672 673

  // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  // (also used by many examples)
674
  const Hash256(0xE14AA40904929BF3, 0x13FDED22CF7E7FFC, 0xBF1D1AAC4263B5EF, 0x1BE8BFCE650397AA),
675 676 677

  // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  // (also used by many examples)
678
  const Hash256(0x4D470BF22D5C17D8, 0x4EDC5F82516D1BA8, 0xA1C09559CD761CEF, 0xB792F86D9F52B540),
679 680 681

  // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  // (also used by many examples)
682
  const Hash256(0x3C34E1F298D0C9EA, 0x3455D46DB6B7759C, 0x8211A49E9EC6E44B, 0x635FC5C87DFB4180),
683 684 685 686

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  // (also used by a few examples)
687
  const Hash256(0x7770183009E91411, 0x2DE7D8EF1D235A6A, 0x30C5834424858E0D, 0x2F8253F6B8D31926),
688 689 690 691

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  // (also used by many examples)
692
  const Hash256(0x5925DAB509451F9E, 0xCBB12CE8A625F9D4, 0xC104718EE20CAFF8, 0xB1B51032D1CD8946),
693 694 695 696 697 698

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  // (also used by many examples)
699
  const Hash256(0xC4D9A284C12301D0, 0xF50E248EC53ED51A, 0x19A10147B774B233, 0x08399250B0D44C55),
700 701 702 703

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  // (also used by many examples)
704
  const Hash256(0xBF97F9D3233F33E1, 0x389B09F7B8ADD537, 0x41300CB834D6C7A5, 0xCA32CBED363A4FB2),
705 706 707 708

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  // (also used by many examples)
709
  const Hash256(0x285442F69A06B45D, 0x9D79DF80321815B5, 0x46473548A37B7881, 0x9B68959C7B8ED237),
710 711 712 713

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  // (also used by many examples)
714
  const Hash256(0x2AB64AF8AC727EA9, 0x9C6AB9EAFF847F46, 0xFBF2A9A0A78A0ABC, 0xBF3180F3851645B4),
715 716 717 718

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  // (also used by many examples)
719
  const Hash256(0x9DCA09F4E5ED5684, 0xD3C4DFF41F4E8B7C, 0xB864B438172D72BE, 0x069315FA362930F9),
720 721 722 723

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  // (also used by many examples)
724
  const Hash256(0xD5AD04DE321EF37C, 0xACC5A7B960AFCCE7, 0x1BDCB96FA020C482, 0x49C1545DD1A0F497),
725 726 727 728 729 730

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  // (also used by many examples)
731
  const Hash256(0x809ABFE75C440770, 0xC13C4E2E46D09603, 0xC22053E9D4E0E227, 0x5DCB9C1DCFBB2C75),
732 733 734 735

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  // (also used by many examples)
736
  const Hash256(0x3DB08CB79E7B01B9, 0xE81F956E3A0AE101, 0x48D0FAFDE3EA7AA7, 0x0048DF905AA52CFD),
737 738 739 740

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  // (also used by many examples)
741
  const Hash256(0x23C13D463F5DCA5C, 0x1F14A14934003601, 0xC29F1218FD461016, 0xD8A22CEF579A665F),
742 743 744 745

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  // (also used by many examples)
746
  const Hash256(0x6DB7726530D71D3F, 0x52CB59793EB69131, 0x3BAA04796E129E1E, 0x043C0A58A1BFFD2F),
747 748 749 750

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  // (also used by many examples)
751
  const Hash256(0xCEE565F5E6211656, 0x9B64980B209FD5CA, 0x4B3D3739011F5343, 0x250B33A1A2C6EB65),
752 753 754 755 756 757 758 759

  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  // (also used by many examples)
760
  const Hash256(0x93AE7D494FAD0FB3, 0x0CBF3AE746A39C4B, 0xC7A0F8BBF87FBB58, 0x7A3F3C01F3C5CE20),
761 762 763

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
  // (also used by a few examples)
764
  const Hash256(0xB18BEBAAD1AD6724, 0xE48BCDF699BA3927, 0xDF3F258FEBE646A3, 0xAB5C62767C6BAB40),
765 766 767

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
  // (also used by a few examples)
768
  const Hash256(0xF90D839A289ECADB, 0xF2B0B3400DA43EB8, 0x08B84908335AE4A0, 0x07457C4D5A56A57C),
769 770 771

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
  // (also used by a few examples)
772
  const Hash256(0x592C2ABF84ADB2D3, 0x91AED8B634D3233E, 0x2C65369F06018DCD, 0x8A4B27BA755EDCBE),
773 774 775

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
  // (also used by a few examples)
776
  const Hash256(0x75D9A0C034113CA8, 0xA1EC11C24B81F208, 0x6630A5A5C65C7D26, 0xA5DC03A1C0A4478C),
777 778 779

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
  // (also used by a few examples)
780
  const Hash256(0xA896E65745557732, 0xC72BD4EE3A10782F, 0xE2AA95590B5AF659, 0x869E5808DB9C01C1),
781 782 783

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
  // (also used by a few examples)
784
  const Hash256(0x3A69A8A1AAC5D9A8, 0x374492AF4B6D07A4, 0xCE637659EB24A784, 0x9C4DFB261D75C6A3),
785 786 787

  // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
  // (also used by a few examples)
788
  const Hash256(0xD29D4E0AF9256DC9, 0x2D0A8F8810608A5E, 0x64A132AD8B397CA2, 0xC4DDC0B1C26A68C3),
789

790
  // packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl
791
  // dev/integration_tests/flutter_gallery/web/icons/Icon-192.png
792
  const Hash256(0x3DCE99077602F704, 0x21C1C6B2A240BC9B, 0x83D64D86681D45F2, 0x154143310C980BE3),
793 794

  // packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl
795
  // dev/integration_tests/flutter_gallery/web/icons/Icon-512.png
796
  const Hash256(0xBACCB205AE45f0B4, 0x21BE1657259B4943, 0xAC40C95094AB877F, 0x3BCBE12CD544DCBE),
797

798
  // packages/flutter_tools/templates/app/web/favicon.png.copy.tmpl
799
  // dev/integration_tests/flutter_gallery/web/favicon.png
800
  const Hash256(0x7AB2525F4B86B65D, 0x3E4C70358A17E5A1, 0xAAF6F437f99CBCC0, 0x46DAD73d59BB9015),
801

802 803
  // GALLERY ICONS

804
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png
805
  const Hash256(0x03CFDE53C249475C, 0x277E8B8E90AC8A13, 0xE5FC13C358A94CCB, 0x67CA866C9862A0DD),
806

807
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png
808
  const Hash256(0x86A83E23A505EFCC, 0x39C358B699EDE12F, 0xC088EE516A1D0C73, 0xF3B5D74DDAD164B1),
809

810
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
811
  const Hash256(0xD813B1A77320355E, 0xB68C485CD47D0F0F, 0x3C7E1910DCD46F08, 0x60A6401B8DC13647),
812

813
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png
814
  const Hash256(0x35AFA76BD5D6053F, 0xEE927436C78A8794, 0xA8BA5F5D9FC9653B, 0xE5B96567BB7215ED),
815

816
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png
817
  const Hash256(0x263CE9B4F1F69B43, 0xEBB08AE9FE8F80E7, 0x95647A59EF2C040B, 0xA8AEB246861A7DFF),
818

819
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
820
  const Hash256(0x5E1A93C3653BAAFF, 0x1AAC6BCEB8DCBC2F, 0x2AE7D68ECB07E507, 0xCB1FA8354B28313A),
821

822
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png
823
  const Hash256(0xA5C77499151DDEC6, 0xDB40D0AC7321FD74, 0x0646C0C0F786743F, 0x8F3C3C408CAC5E8C),
824

825
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png
826
  const Hash256(0x33DE450980A2A16B, 0x1982AC7CDC1E7B01, 0x919E07E0289C2139, 0x65F85BCED8895FEF),
827

828
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
829
  const Hash256(0xC3B8577F4A89BA03, 0x830944FB06C3566B, 0x4C99140A2CA52958, 0x089BFDC3079C59B7),
830

831
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png
832
  const Hash256(0xDEBC241D6F9C5767, 0x8980FDD46FA7ED0C, 0x5B8ACD26BCC5E1BC, 0x473C89B432D467AD),
833

834
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png
835
  const Hash256(0xBEFE5F7E82BF8B64, 0x148D869E3742004B, 0xF821A9F5A1BCDC00, 0x357D246DCC659DC2),
836

837
  // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
838
  const Hash256(0xC385404341FF9EDD, 0x30FBE76F0EC99155, 0x8EA4F4AFE8CC0C60, 0x1CA3EDEF177E1DA8),
839

840
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
841
  const Hash256(0x6BE5751A29F57A80, 0x36A4B31CC542C749, 0x984E49B22BD65CAA, 0x75AE8B2440848719),
842

843
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png
844
  const Hash256(0x9972A2264BFA8F8D, 0x964AFE799EADC1FA, 0x2247FB31097F994A, 0x1495DC32DF071793),
845

846
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png
847
  const Hash256(0x4C7CC9B09BEEDA24, 0x45F57D6967753910, 0x57D68E1A6B883D2C, 0x8C52701A74F1400F),
848

849
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-167.png
850
  const Hash256(0x66DACAC1CFE4D349, 0xDBE994CB9125FFD7, 0x2D795CFC9CF9F739, 0xEDBB06CE25082E9C),
851

852
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-180.png
853
  const Hash256(0x5188621015EBC327, 0xC9EF63AD76E60ECE, 0xE82BDC3E4ABF09E2, 0xEE0139FA7C0A2BE5),
854

855
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20.png
856
  const Hash256(0x27D2752D04EE9A6B, 0x78410E208F74A6CD, 0xC90D9E03B73B8C60, 0xD05F7D623E790487),
857

858
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29.png
859
  const Hash256(0xBB20556B2826CF85, 0xD5BAC73AA69C2AC3, 0x8E71DAD64F15B855, 0xB30CB73E0AF89307),
860

861
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40.png
862
  const Hash256(0x623820FA45CDB0AC, 0x808403E34AD6A53E, 0xA3E9FDAE83EE0931, 0xB020A3A4EF2CDDE7),
863

864
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-58.png
865
  const Hash256(0xC6D631D1E107215E, 0xD4A58FEC5F3AA4B5, 0x0AE9724E07114C0C, 0x453E5D87C2CAD3B3),
866

867
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png
868
  const Hash256(0x4B6F58D1EB8723C6, 0xE717A0D09FEC8806, 0x90C6D1EF4F71836E, 0x618672827979B1A2),
869

870
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
871
  const Hash256(0x0A1744CC7634D508, 0xE85DD793331F0C8A, 0x0B7C6DDFE0975D8F, 0x29E91C905BBB1BED),
872

873
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-80.png
874
  const Hash256(0x24032FBD1E6519D6, 0x0BA93C0D5C189554, 0xF50EAE23756518A2, 0x3FABACF4BD5DAF08),
875

876
  // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-87.png
877
  const Hash256(0xC17BAE6DF6BB234A, 0xE0AF4BEB0B805F12, 0x14E74EB7AA9A30F1, 0x5763689165DA7DDF),
878 879 880 881


  // STOCKS ICONS

882
  // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
883
  const Hash256(0x74052AB5241D4418, 0x7085180608BC3114, 0xD12493C50CD8BBC7, 0x56DED186C37ACE84),
884

885
  // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
886
  const Hash256(0xE37947332E3491CB, 0x82920EE86A086FEA, 0xE1E0A70B3700A7DA, 0xDCAFBDD8F40E2E19),
887

888
  // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
889
  const Hash256(0xE608CDFC0C8579FB, 0xE38873BAAF7BC944, 0x9C9D2EE3685A4FAE, 0x671EF0C8BC41D17C),
890

891
  // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
892
  const Hash256(0xBD53D86977DF9C54, 0xF605743C5ABA114C, 0x9D51D1A8BB917E1A, 0x14CAA26C335CAEBD),
893

894
  // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
895
  const Hash256(0x64E4D02262C4F3D0, 0xBB4FDC21CD0A816C, 0x4CD2A0194E00FB0F, 0x1C3AE4142FAC0D15),
896

897 898
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
899
  const Hash256(0x5BA3283A76918FC0, 0xEE127D0F22D7A0B6, 0xDF03DAED61669427, 0x93D89DDD87A08117),
900

901
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
902
  const Hash256(0xCD7F26ED31DEA42A, 0x535D155EC6261499, 0x34E6738255FDB2C4, 0xBD8D4BDDE9A99B05),
903

904
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
905
  const Hash256(0x3FA1225FC9A96A7E, 0xCD071BC42881AB0E, 0x7747EB72FFB72459, 0xA37971BBAD27EE24),
906

907
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
908
  const Hash256(0xCD867001ACD7BBDB, 0x25CDFD452AE89FA2, 0x8C2DC980CAF55F48, 0x0B16C246CFB389BC),
909

910
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
911
  const Hash256(0x848E9736E5C4915A, 0x7945BCF6B32FD56B, 0x1F1E7CDDD914352E, 0xC9681D38EF2A70DA),
912

913
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png
914
  const Hash256(0x654BA7D6C4E05CA0, 0x7799878884EF8F11, 0xA383E1F24CEF5568, 0x3C47604A966983C8),
915

916 917
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
918
  const Hash256(0x743056FE7D83FE42, 0xA2990825B6AD0415, 0x1AF73D0D43B227AA, 0x07EBEA9B767381D9),
919

920
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png
921
  const Hash256(0xA7E1570812D119CF, 0xEF4B602EF28DD0A4, 0x100D066E66F5B9B9, 0x881765DC9303343B),
922

923
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
924
  const Hash256(0xB4102839A1E41671, 0x62DACBDEFA471953, 0xB1EE89A0AB7594BE, 0x1D9AC1E67DC2B2CE),
925

926
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
927
  const Hash256(0x70AC6571B593A967, 0xF1CBAEC9BC02D02D, 0x93AD766D8290ADE6, 0x840139BF9F219019),
928

929
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
930
  const Hash256(0x5D87A78386DA2C43, 0xDDA8FEF2CA51438C, 0xE5A276FE28C6CF0A, 0xEBE89085B56665B6),
931

932
  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
933
  const Hash256(0x4D9F5E81F668DA44, 0xB20A77F8BF7BA2E1, 0xF384533B5AD58F07, 0xB3A2F93F8635CD96),
934 935 936 937 938 939 940 941


  // LEGACY ICONS

  // dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
  // dev/benchmarks/microbenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
  // examples/flutter_view/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
  // (not really sure where this came from, or why neither the template nor most examples use them)
942
  const Hash256(0x6E645DC9ED913AAD, 0xB50ED29EEB16830D, 0xB32CA12F39121DB9, 0xB7BC1449DDDBF8B8),
943 944 945 946 947

  // dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  // dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  // dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  // dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
948
  const Hash256(0xDEFAC77E08EC71EC, 0xA04CCA3C95D1FC33, 0xB9F26E1CB15CB051, 0x47DEFC79CDD7C158),
949 950 951

  // examples/flutter_view/ios/Runner/ic_add.png
  // examples/platform_view/ios/Runner/ic_add.png
952
  const Hash256(0x3CCE7450334675E2, 0xE3AABCA20B028993, 0x127BE82FE0EB3DFF, 0x8B027B3BAF052F2F),
953 954

  // examples/image_list/images/coast.jpg
955
  const Hash256(0xDA957FD30C51B8D2, 0x7D74C2C918692DC4, 0xD3C5C99BB00F0D6B, 0x5EBB30395A6EDE82),
956 957

  // examples/image_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
958
  const Hash256(0xB5792CA06F48A431, 0xD4379ABA2160BD5D, 0xE92339FC64C6A0D3, 0x417AA359634CD905),
959 960 961 962 963


  // TEST ASSETS

  // dev/benchmarks/macrobenchmarks/assets/999x1000.png
964
  const Hash256(0x553E9C36DFF3E610, 0x6A608BDE822A0019, 0xDE4F1769B6FBDB97, 0xBC3C20E26B839F59),
965 966

  // dev/bots/test/analyze-test-input/root/packages/foo/serviceaccount.enc
967
  const Hash256(0xA8100AE6AA1940D0, 0xB663BB31CD466142, 0xEBBDBD5187131B92, 0xD93818987832EB89),
968 969

  // dev/automated_tests/icon/test.png
970
  const Hash256(0xE214B4A0FEEEC6FA, 0x8E7AA8CC9BFBEC40, 0xBCDAC2F2DEBC950F, 0x75AF8EBF02BCE459),
971 972 973

  // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-land-xxhdpi/flutter_splash_screen.png
  // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-land-xxhdpi/flutter_splash_screen.png
974
  const Hash256(0x2D4F8D7A3DFEF9D3, 0xA0C66938E169AB58, 0x8C6BBBBD1973E34E, 0x03C428416D010182),
975 976 977

  // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-xxhdpi/flutter_splash_screen.png
  // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-xxhdpi/flutter_splash_screen.png
978
  const Hash256(0xCD46C01BAFA3B243, 0xA6AA1645EEDDE481, 0x143AC8ABAB1A0996, 0x22CAA9D41F74649A),
979 980

  // dev/integration_tests/flutter_driver_screenshot_test/assets/red_square.png
981
  const Hash256(0x40054377E1E084F4, 0x4F4410CE8F44C210, 0xABA945DFC55ED0EF, 0x23BDF9469E32F8D3),
982 983

  // dev/integration_tests/flutter_driver_screenshot_test/test_driver/goldens/red_square_image/iPhone7,2.png
984
  const Hash256(0x7F9D27C7BC418284, 0x01214E21CA886B2F, 0x40D9DA2B31AE7754, 0x71D68375F9C8A824),
985 986 987

  // examples/flutter_view/assets/flutter-mark-square-64.png
  // examples/platform_view/assets/flutter-mark-square-64.png
988
  const Hash256(0xF416B0D8AC552EC8, 0x819D1F492D1AB5E6, 0xD4F20CF45DB47C22, 0x7BB431FEFB5B67B2),
989 990

  // packages/flutter_tools/test/data/intellij/plugins/Dart/lib/Dart.jar
991
  const Hash256(0x576E489D788A13DB, 0xBF40E4A39A3DAB37, 0x15CCF0002032E79C, 0xD260C69B29E06646),
992 993

  // packages/flutter_tools/test/data/intellij/plugins/flutter-intellij.jar
994
  const Hash256(0x4C67221E25626CB2, 0x3F94E1F49D34E4CF, 0x3A9787A514924FC5, 0x9EF1E143E5BC5690),
995 996 997 998 999


  // MISCELLANEOUS

  // dev/bots/serviceaccount.enc
1000
  const Hash256(0x1F19ADB4D80AFE8C, 0xE61899BA776B1A8D, 0xCA398C75F5F7050D, 0xFB0E72D7FBBBA69B),
1001 1002

  // dev/docs/favicon.ico
1003
  const Hash256(0x67368CA1733E933A, 0xCA3BC56EF0695012, 0xE862C371AD4412F0, 0x3EC396039C609965),
1004 1005

  // dev/snippets/assets/code_sample.png
1006
  const Hash256(0xAB2211A47BDA001D, 0x173A52FD9C75EBC7, 0xE158942FFA8243AD, 0x2A148871990D4297),
1007 1008

  // dev/snippets/assets/code_snippet.png
1009
  const Hash256(0xDEC70574DA46DFBB, 0xFA657A771F3E1FBD, 0xB265CFC6B2AA5FE3, 0x93BA4F325D1520BA),
1010 1011

  // packages/flutter_tools/static/Ahem.ttf
1012
  const Hash256(0x63D2ABD0041C3E3B, 0x4B52AD8D382353B5, 0x3C51C6785E76CE56, 0xED9DACAD2D2E31C4),
1013 1014
};

1015 1016
Future<void> verifyNoBinaries(String workingDirectory, { Set<Hash256> legacyBinaries }) async {
  // Please do not add anything to the _legacyBinaries set above.
1017
  // We have a policy of not checking in binaries into this repository.
1018 1019 1020
  // If you are adding/changing template images, use the flutter_template_images
  // package and a .img.tmpl placeholder instead.
  // If you have other binaries to add, please consult Hixie for advice.
1021
  assert(
1022
    _legacyBinaries
1023
      .expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d])
1024
      .reduce((int value, int element) => value ^ element) == 0x606B51C908B40BFA // Please do not modify this line.
1025
  );
1026
  legacyBinaries ??= _legacyBinaries;
1027
  if (!Platform.isWindows) { // TODO(ianh): Port this to Windows
1028
    final List<File> files = await _gitFiles(workingDirectory, runSilently: false);
1029
    final List<String> problems = <String>[];
1030
    for (final File file in files) {
1031 1032 1033 1034
      final Uint8List bytes = file.readAsBytesSync();
      try {
        utf8.decode(bytes);
      } on FormatException catch (error) {
1035
        final Digest digest = sha256.convert(bytes);
1036
        if (!legacyBinaries.contains(Hash256.fromDigest(digest)))
1037
          problems.add('${file.path}:${error.offset}: file is not valid UTF-8');
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048
      }
    }
    if (problems.isNotEmpty) {
      exitWithError(<String>[
        ...problems,
        'All files in this repository must be UTF-8. In particular, images and other binaries',
        'must not be checked into this repository. This is because we are very sensitive to the',
        'size of the repository as it is distributed to all our developers. If you have a binary',
        'to which you need access, you should consider how to fetch it from another repository;',
        'for example, the "assets-for-api-docs" repository is used for images in API docs.',
      ]);
1049 1050 1051 1052 1053 1054 1055 1056
    }
  }
}


// UTILITY FUNCTIONS

bool _listEquals<T>(List<T> a, List<T> b) {
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
  assert(a != null);
  assert(b != null);
  if (a.length != b.length)
    return false;
  for (int index = 0; index < a.length; index += 1) {
    if (a[index] != b[index])
      return false;
  }
  return true;
}

1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) async {
  final EvalResult evalResult = await _evalCommand(
    'git', <String>['ls-files', '-z'],
    workingDirectory: workingDirectory,
    runSilently: runSilently,
  );
  if (evalResult.exitCode != 0) {
    exitWithError(<String>[
      'git ls-filese failed with exit code ${evalResult.exitCode}',
      '${bold}stdout:$reset',
      evalResult.stdout,
      '${bold}stderr:$reset',
      evalResult.stderr,
    ]);
  }
  final List<String> filenames = evalResult
      .stdout
      .split('\x00');
  assert(filenames.last.isEmpty); // git ls-files gives a trailing blank 0x00
  filenames.removeLast();
  return filenames
      .map<File>((String filename) => File(path.join(workingDirectory, filename)))
      .toList();
}

Stream<File> _allFiles(String workingDirectory, String extension, { @required int minimumMatches }) async* {
  final Set<String> gitFileNamesSet = <String>{};
  gitFileNamesSet.addAll((await _gitFiles(workingDirectory)).map((File f) => path.canonicalize(f.absolute.path)));

1097
  assert(extension == null || !extension.startsWith('.'), 'Extension argument should not start with a period.');
1098
  final Set<FileSystemEntity> pending = <FileSystemEntity>{ Directory(workingDirectory) };
1099
  int matches = 0;
1100 1101 1102
  while (pending.isNotEmpty) {
    final FileSystemEntity entity = pending.first;
    pending.remove(entity);
Ian Hickson's avatar
Ian Hickson committed
1103 1104
    if (path.extension(entity.path) == '.tmpl')
      continue;
1105
    if (entity is File) {
1106 1107
      if (!gitFileNamesSet.contains(path.canonicalize(entity.absolute.path)))
        continue;
Ian Hickson's avatar
Ian Hickson committed
1108 1109 1110 1111 1112 1113
      if (_isGeneratedPluginRegistrant(entity))
        continue;
      if (path.basename(entity.path) == 'flutter_export_environment.sh')
        continue;
      if (path.basename(entity.path) == 'gradlew.bat')
        continue;
1114 1115
      if (path.basename(entity.path) == '.DS_Store')
        continue;
1116 1117
      if (extension == null || path.extension(entity.path) == '.$extension') {
        matches += 1;
1118
        yield entity;
1119
      }
1120 1121 1122 1123 1124
    } else if (entity is Directory) {
      if (File(path.join(entity.path, '.dartignore')).existsSync())
        continue;
      if (path.basename(entity.path) == '.git')
        continue;
1125 1126
      if (path.basename(entity.path) == '.idea')
        continue;
1127 1128
      if (path.basename(entity.path) == '.gradle')
        continue;
1129 1130
      if (path.basename(entity.path) == '.dart_tool')
        continue;
1131 1132
      if (path.basename(entity.path) == '.idea')
        continue;
Ian Hickson's avatar
Ian Hickson committed
1133 1134
      if (path.basename(entity.path) == 'build')
        continue;
1135 1136 1137
      pending.addAll(entity.listSync());
    }
  }
1138
  assert(matches >= minimumMatches, 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.');
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
}

class EvalResult {
  EvalResult({
    this.stdout,
    this.stderr,
    this.exitCode = 0,
  });

  final String stdout;
  final String stderr;
  final int exitCode;
}

1153
// TODO(ianh): Refactor this to reuse the code in run_command.dart
1154 1155 1156 1157 1158
Future<EvalResult> _evalCommand(String executable, List<String> arguments, {
  @required String workingDirectory,
  Map<String, String> environment,
  bool skip = false,
  bool allowNonZeroExit = false,
1159
  bool runSilently = false,
1160 1161 1162 1163 1164 1165 1166
}) async {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);
  if (skip) {
    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
    return null;
  }
1167 1168 1169 1170

  if (!runSilently) {
    printProgress('RUNNING', relativeWorkingDir, commandDescription);
  }
1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186

  final Stopwatch time = Stopwatch()..start();
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

  final Future<List<List<int>>> savedStdout = process.stdout.toList();
  final Future<List<List<int>>> savedStderr = process.stderr.toList();
  final int exitCode = await process.exitCode;
  final EvalResult result = EvalResult(
    stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()),
    stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()),
    exitCode: exitCode,
  );

1187 1188 1189
  if (!runSilently) {
    print('$clock ELAPSED TIME: $bold${prettyPrintDuration(time.elapsed)}$reset for $commandDescription in $relativeWorkingDir');
  }
1190 1191 1192

  if (exitCode != 0 && !allowNonZeroExit) {
    stderr.write(result.stderr);
1193 1194 1195 1196 1197
    exitWithError(<String>[
      '${bold}ERROR:$red Last command exited with $exitCode.$reset',
      '${bold}Command:$red $commandDescription$reset',
      '${bold}Relative working directory:$red $relativeWorkingDir$reset',
    ]);
1198 1199 1200 1201 1202 1203 1204
  }

  return result;
}

Future<void> _runFlutterAnalyze(String workingDirectory, {
  List<String> options = const <String>[],
1205 1206
}) async {
  return await runCommand(
1207 1208 1209 1210 1211 1212
    flutter,
    <String>['analyze', '--dartdocs', ...options],
    workingDirectory: workingDirectory,
  );
}

1213
final RegExp _importPattern = RegExp(r'''^\s*import (['"])package:flutter/([^.]+)\.dart\1''');
1214
final RegExp _importMetaPattern = RegExp(r'''^\s*import (['"])package:meta/meta\.dart\1''');
1215

1216 1217
Future<Set<String>> _findFlutterDependencies(String srcPath, List<String> errors, { bool checkForMeta = false }) async {
  return await _allFiles(srcPath, 'dart', minimumMatches: 1)
1218 1219
    .map<Set<String>>((File file) {
      final Set<String> result = <String>{};
1220
      for (final String line in file.readAsLinesSync()) {
1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231
        Match match = _importPattern.firstMatch(line);
        if (match != null)
          result.add(match.group(2));
        if (checkForMeta) {
          match = _importMetaPattern.firstMatch(line);
          if (match != null) {
            errors.add(
              '${file.path}\nThis package imports the ${yellow}meta$reset package.\n'
              'You should instead import the "foundation.dart" library.'
            );
          }
1232 1233
        }
      }
1234 1235 1236 1237 1238 1239 1240
      return result;
    })
    .reduce((Set<String> value, Set<String> element) {
      value ??= <String>{};
      value.addAll(element);
      return value;
    });
1241 1242 1243
}

List<T> _deepSearch<T>(Map<T, Set<T>> map, T start, [ Set<T> seen ]) {
1244 1245 1246
  if (map[start] == null)
    return null; // We catch these separately.

1247
  for (final T key in map[start]) {
1248 1249 1250 1251
    if (key == start)
      continue; // we catch these separately
    if (seen != null && seen.contains(key))
      return <T>[start, key];
1252
    final List<T> result = _deepSearch<T>(
1253 1254
      map,
      key,
1255 1256 1257 1258
      <T>{
        if (seen == null) start else ...seen,
        key,
      },
1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277
    );
    if (result != null) {
      result.insert(0, start);
      // Only report the shortest chains.
      // For example a->b->a, rather than c->a->b->a.
      // Since we visit every node, we know the shortest chains are those
      // that start and end on the loop.
      if (result.first == result.last)
        return result;
    }
  }
  return null;
}

bool _isGeneratedPluginRegistrant(File file) {
  final String filename = path.basename(file.path);
  return !file.path.contains('.pub-cache')
      && (filename == 'GeneratedPluginRegistrant.java' ||
          filename == 'GeneratedPluginRegistrant.h' ||
1278
          filename == 'GeneratedPluginRegistrant.m' ||
1279 1280
          filename == 'generated_plugin_registrant.dart' ||
          filename == 'generated_plugin_registrant.h');
1281
}