// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:core' hide print;
import 'dart:io' hide exit;
import 'dart:typed_data';

import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';

import 'allowlist.dart';
import 'run_command.dart';
import 'utils.dart';

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 flutterPackages = path.join(flutterRoot, 'packages');
final String flutterExamples = path.join(flutterRoot, 'examples');

/// The path to the `dart` executable; set at the top of `main`
late final String dart;

/// The path to the `pub` executable; set at the top of `main`
late final String 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
Future<void> main(List<String> arguments) async {
  final String dartSdk = path.join(
    Directory.current.absolute.path,
    _getDartSdkFromArguments(arguments) ?? path.join(flutterRoot, 'bin', 'cache', 'dart-sdk'),
  );
  dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
  pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
  print('$clock STARTING ANALYSIS');
  try {
    await run(arguments);
  } on ExitException catch (error) {
    error.apply();
  }
  print('$clock ${bold}Analysis successful.$reset');
}

/// Scans [arguments] for an argument of the form `--dart-sdk` or
/// `--dart-sdk=...` and returns the configured SDK, if any.
String? _getDartSdkFromArguments(List<String> arguments) {
  String? result;
  for (int i = 0; i < arguments.length; i += 1) {
    if (arguments[i] == '--dart-sdk') {
      if (result != null) {
        exitWithError(<String>['The --dart-sdk argument must not be used more than once.']);
      }
      if (i + 1 < arguments.length) {
        result = arguments[i + 1];
      } else {
        exitWithError(<String>['--dart-sdk must be followed by a path.']);
      }
    }
    if (arguments[i].startsWith('--dart-sdk=')) {
      if (result != null) {
        exitWithError(<String>['The --dart-sdk argument must not be used more than once.']);
      }
      result = arguments[i].substring('--dart-sdk='.length);
    }
  }
  return result;
}

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

  print('$clock No Double.clamp');
  await verifyNoDoubleClamp(flutterRoot);

  print('$clock All tool test files end in _test.dart...');
  await verifyToolTestsEndInTestDart(flutterRoot);

  print('$clock No sync*/async*');
  await verifyNoSyncAsyncStar(flutterPackages);
  await verifyNoSyncAsyncStar(flutterExamples, minimumMatches: 200);

  print('$clock No runtimeType in toString...');
  await verifyNoRuntimeTypeInToString(flutterRoot);

  print('$clock Debug mode instead of checked mode...');
  await verifyNoCheckedMode(flutterRoot);

  print('$clock Links for creating GitHub issues');
  await verifyIssueLinks(flutterRoot);

  print('$clock Unexpected binaries...');
  await verifyNoBinaries(flutterRoot);

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

  print('$clock Deprecations...');
  await verifyDeprecations(flutterRoot);

  print('$clock Goldens...');
  await verifyGoldenTags(flutterPackages);

  print('$clock Skip test comments...');
  await verifySkipTestComments(flutterRoot);

  print('$clock Licenses...');
  await verifyNoMissingLicense(flutterRoot);

  print('$clock Test imports...');
  await verifyNoTestImports(flutterRoot);

  print('$clock Bad imports (framework)...');
  await verifyNoBadImportsInFlutter(flutterRoot);

  print('$clock Bad imports (tools)...');
  await verifyNoBadImportsInFlutterTools(flutterRoot);

  print('$clock Internationalization...');
  await verifyInternationalizations(flutterRoot, dart);

  print('$clock Integration test timeouts...');
  await verifyIntegrationTestTimeouts(flutterRoot);

  print('$clock null initialized debug fields...');
  await verifyNullInitializedDebugExpensiveFields(flutterRoot);

  // Ensure that all package dependencies are in sync.
  print('$clock Package dependencies...');
  await runCommand(flutter, <String>['update-packages', '--verify-only'],
    workingDirectory: flutterRoot,
  );

  /// Ensure that no new dependencies have been accidentally
  /// added to core packages.
  print('$clock Package Allowlist...');
  await _checkConsumerDependencies();

  // Analyze all the Dart code in the repo.
  print('$clock Dart analysis...');
  await _runFlutterAnalyze(flutterRoot, options: <String>[
    '--flutter-repo',
    ...arguments,
  ]);

  print('$clock Executable allowlist...');
  await _checkForNewExecutables();

  // Try with the --watch analyzer, to make sure it returns success also.
  // The --benchmark argument exits after one run.
  print('$clock Dart analysis (with --watch)...');
  await _runFlutterAnalyze(flutterRoot, options: <String>[
    '--flutter-repo',
    '--watch',
    '--benchmark',
    ...arguments,
  ]);

  // Analyze the code in `{@tool snippet}` sections in the repo.
  print('$clock Snippet code...');
  await runCommand(dart,
    <String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'), '--verbose'],
    workingDirectory: flutterRoot,
  );

  // Try analysis against a big version of the gallery; generate into a temporary directory.
  print('$clock Dart analysis (mega gallery)...');
  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,
    );
    await _runFlutterAnalyze(outDir.path, options: <String>[
      '--watch',
      '--benchmark',
      ...arguments,
    ]);
  } finally {
    outDir.deleteSync(recursive: true);
  }
}


// TESTS

FeatureSet _parsingFeatureSet() => FeatureSet.fromEnableFlags2(
    sdkLanguageVersion: Version.parse('2.17.0-0'),
    flags: <String>['super-parameters']);

_Line _getLine(ParseStringResult parseResult, int offset) {
  final int lineNumber =
      parseResult.lineInfo.getLocation(offset).lineNumber;
  final String content = parseResult.content.substring(
      parseResult.lineInfo.getOffsetOfLine(lineNumber - 1),
      parseResult.lineInfo.getOffsetOfLine(lineNumber) - 1);
  return _Line(lineNumber, content);
}

class _DoubleClampVisitor extends RecursiveAstVisitor<CompilationUnit> {
  _DoubleClampVisitor(this.parseResult);

  final List<_Line> clamps = <_Line>[];
  final ParseStringResult parseResult;

  @override
  CompilationUnit? visitMethodInvocation(MethodInvocation node) {
    if (node.methodName.name == 'clamp') {
      final _Line line = _getLine(parseResult, node.function.offset);
      if (!line.content.contains('// ignore_clamp_double_lint')) {
        clamps.add(line);
      }
    }

    node.visitChildren(this);
    return null;
  }
}

/// Verify that we use clampDouble instead of Double.clamp for performance reasons.
///
/// We currently can't distinguish valid uses of clamp from problematic ones so
/// if the clamp is operating on a type other than a `double` the
/// `// ignore_clamp_double_lint` comment must be added to the line where clamp is
/// invoked.
///
/// See also:
///   * https://github.com/flutter/flutter/pull/103559
///   * https://github.com/flutter/flutter/issues/103917
Future<void> verifyNoDoubleClamp(String workingDirectory) async {
  final String flutterLibPath = '$workingDirectory/packages/flutter/lib';
  final Stream<File> testFiles =
      _allFiles(flutterLibPath, 'dart', minimumMatches: 100);
  final List<String> errors = <String>[];
  await for (final File file in testFiles) {
    try {
      final ParseStringResult parseResult = parseFile(
        featureSet: _parsingFeatureSet(),
        path: file.absolute.path,
      );
      final _DoubleClampVisitor visitor = _DoubleClampVisitor(parseResult);
      visitor.visitCompilationUnit(parseResult.unit);
      for (final _Line clamp in visitor.clamps) {
        errors.add('${file.path}:${clamp.line}: `clamp` method used without ignore_clamp_double_lint comment.');
      }
    } catch (ex) {
      // TODO(gaaclarke): There is a bug with super parameter parsing on mac so
      // we skip certain files until that is fixed.
      // https://github.com/dart-lang/sdk/issues/49032
      print('skipping ${file.path}: $ex');
    }
  }
  if (errors.isNotEmpty) {
    exitWithError(<String>[
      ...errors,
      '\n${bold}See: https://github.com/flutter/flutter/pull/103559',
    ]);
  }
}

/// Verify tool test files end in `_test.dart`.
///
/// The test runner will only recognize files ending in `_test.dart` as tests to
/// be run: https://github.com/dart-lang/test/tree/master/pkgs/test#running-tests
Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async {
  final String toolsTestPath = path.join(
    workingDirectory,
    'packages',
    'flutter_tools',
    'test',
  );
  final List<String> violations = <String>[];

  // detect files that contains calls to test(), testUsingContext(), and testWithoutContext()
  final RegExp callsTestFunctionPattern = RegExp(r'(test\(.*\)|testUsingContext\(.*\)|testWithoutContext\(.*\))');

  await for (final File file in _allFiles(toolsTestPath, 'dart', minimumMatches: 300)) {
    final bool isValidTestFile = file.path.endsWith('_test.dart');
    if (isValidTestFile) {
      continue;
    }

    final bool isTestData = file.path.contains(r'test_data');
    if (isTestData) {
      continue;
    }

    final bool isInTestShard = file.path.contains(r'.shard/');
    if (!isInTestShard) {
      continue;
    }

    final bool callsTestFunction = file.readAsStringSync().contains(callsTestFunctionPattern);
    if (!callsTestFunction) {
      continue;
    }

    violations.add(file.path);
  }
  if (violations.isNotEmpty) {
    exitWithError(<String>[
      '${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset',
      ...violations,
    ]);
  }
}

Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches = 2000 }) async {
  final RegExp syncPattern = RegExp(r'\s*?a?sync\*\s*?{');
  final RegExp ignorePattern = RegExp(r'^\s*?// The following uses a?sync\* because:? ');
  final RegExp commentPattern = RegExp(r'^\s*?//');
  final List<String> errors = <String>[];
  await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
    if (file.path.contains('test')) {
      continue;
    }
    final List<String> lines = file.readAsLinesSync();
    for (int index = 0; index < lines.length; index += 1) {
      final String line = lines[index];
      if (line.startsWith(commentPattern)) {
        continue;
      }
      if (line.contains(syncPattern)) {
        int lookBehindIndex = index - 1;
        bool hasExplanation = false;
        while (lookBehindIndex >= 0 && lines[lookBehindIndex].startsWith(commentPattern)) {
          if (lines[lookBehindIndex].startsWith(ignorePattern)) {
            hasExplanation = true;
            break;
          }
          lookBehindIndex -= 1;
        }
        if (!hasExplanation) {
          errors.add('${file.path}:$index: sync*/async* without an explanation.');
        }
      }
    }
  }
  if (errors.isNotEmpty) {
    exitWithError(<String>[
      '${bold}Do not use sync*/async* methods. See https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-syncasync for details.$reset',
      ...errors,
    ]);
  }
}

final RegExp _findGoldenTestPattern = RegExp(r'matchesGoldenFile\(');
final RegExp _findGoldenDefinitionPattern = RegExp(r'matchesGoldenFile\(Object');
final RegExp _leadingComment = RegExp(r'//');
final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
final RegExp _goldenTagPattern2 = RegExp(r"'reduced-test-set'");

/// Only golden file tests in the flutter package are subject to reduced testing,
/// for example, invocations in flutter_test to validate comparator
/// functionality do not require tagging.
const String _ignoreGoldenTag = '// flutter_ignore: golden_tag (see analyze.dart)';
const String _ignoreGoldenTagForFile = '// flutter_ignore_for_file: golden_tag (see analyze.dart)';

Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 2000 }) async {
  final List<String> errors = <String>[];
  await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
    bool needsTag = false;
    bool hasTagNotation = false;
    bool hasReducedTag = false;
    bool ignoreForFile = false;
    final List<String> lines = file.readAsLinesSync();
    for (final String line in lines) {
      if (line.contains(_goldenTagPattern1)) {
        hasTagNotation = true;
      }
      if (line.contains(_goldenTagPattern2)) {
        hasReducedTag = true;
      }
      if (line.contains(_findGoldenTestPattern)
          && !line.contains(_findGoldenDefinitionPattern)
          && !line.contains(_leadingComment)
          && !line.contains(_ignoreGoldenTag)) {
        needsTag = true;
      }
      if (line.contains(_ignoreGoldenTagForFile)) {
        ignoreForFile = true;
      }
      // If the file is being ignored or a reduced test tag is already accounted
      // for, skip parsing the rest of the lines for golden file tests.
      if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
        break;
      }
    }
    // If a reduced test tag is already accounted for, move on to the next file.
    if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
      continue;
    }
    // If there are golden file tests, ensure they are tagged for all reduced
    // test environments.
    if (needsTag) {
      if (!hasTagNotation) {
        errors.add('${file.path}: Files containing golden tests must be tagged using '
            '`@Tags(...)` at the top of the file before import statements.');
      } else if (!hasReducedTag) {
        errors.add('${file.path}: Files containing golden tests must be tagged with '
            "'reduced-test-set'.");
      }
    }
  }
  if (errors.isNotEmpty) {
    exitWithError(<String>[
      ...errors,
      '${bold}See: https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter$reset',
    ]);
  }
}

final RegExp _findDeprecationPattern = RegExp(r'@[Dd]eprecated');
final RegExp _deprecationStartPattern = RegExp(r'^(?<indent> *)@Deprecated\($'); // flutter_ignore: deprecation_syntax (see analyze.dart)
final RegExp _deprecationMessagePattern = RegExp(r"^ *'(?<message>.+) '$");
final RegExp _deprecationVersionPattern = RegExp(r"^ *'This feature was deprecated after v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<build>-\d+\.\d+\.pre)?\.',?$");
final RegExp _deprecationEndPattern = 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 = ' // flutter_ignore: deprecation_syntax (see analyze.dart)';

/// Some deprecation notices are exempt for historical reasons. They must have an issue listed.
final RegExp _legacyDeprecation = RegExp(r' // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/\d+$');

Future<void> verifyDeprecations(String workingDirectory, { int minimumMatches = 2000 }) async {
  final List<String> errors = <String>[];
  await for (final File file in _allFiles(workingDirectory, 'dart', minimumMatches: minimumMatches)) {
    int lineNumber = 0;
    final List<String> lines = file.readAsLinesSync();
    final List<int> linesWithDeprecations = <int>[];
    for (final String line in lines) {
      if (line.contains(_findDeprecationPattern) &&
          !line.endsWith(_ignoreDeprecation) &&
          !line.contains(_legacyDeprecation)) {
        linesWithDeprecations.add(lineNumber);
      }
      lineNumber += 1;
    }
    for (int lineNumber in linesWithDeprecations) {
      try {
        final RegExpMatch? startMatch = _deprecationStartPattern.firstMatch(lines[lineNumber]);
        if (startMatch == null)
          throw 'Deprecation notice does not match required pattern.';
        final String indent = startMatch.namedGroup('indent')!;
        lineNumber += 1;
        if (lineNumber >= lines.length)
          throw 'Incomplete deprecation notice.';
        RegExpMatch? versionMatch;
        String? message;
        do {
          final RegExpMatch? messageMatch = _deprecationMessagePattern.firstMatch(lines[lineNumber]);
          if (messageMatch == null) {
            String possibleReason = '';
            if (lines[lineNumber].trimLeft().startsWith('"')) {
              possibleReason = ' You might have used double quotes (") for the string instead of single quotes (\').';
            }
            throw 'Deprecation notice does not match required pattern.$possibleReason';
          }
          if (!lines[lineNumber].startsWith("$indent  '"))
            throw 'Unexpected deprecation notice indent.';
          if (message == null) {
            message = messageMatch.namedGroup('message');
            final String firstChar = String.fromCharCode(message!.runes.first);
            if (firstChar.toUpperCase() != firstChar)
              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';
          }
          lineNumber += 1;
          if (lineNumber >= lines.length)
            throw 'Incomplete deprecation notice.';
          versionMatch = _deprecationVersionPattern.firstMatch(lines[lineNumber]);
        } while (versionMatch == null);
        final int major = int.parse(versionMatch.namedGroup('major')!);
        final int minor = int.parse(versionMatch.namedGroup('minor')!);
        final int patch = int.parse(versionMatch.namedGroup('patch')!);
        final bool hasBuild = versionMatch.namedGroup('build') != null;
        // There was a beta release that was mistakenly labeled 3.1.0 without a build.
        final bool specialBeta = major == 3 && minor == 1 && patch == 0;
        if (!specialBeta && (major > 1 || (major == 1 && minor >= 20))) {
          if (!hasBuild)
            throw 'Deprecation notice does not accurately indicate a beta branch version number; please see https://flutter.dev/docs/development/tools/sdk/releases to find the latest beta build version number.';
        }
        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(_deprecationEndPattern))
          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) {
    exitWithError(<String>[
      ...errors,
      '${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes$reset',
    ]);
  }
}

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.';
}

Future<void> verifyNoMissingLicense(String workingDirectory, { bool checkMinimums = true }) async {
  final int? overrideMinimumMatches = checkMinimums ? null : 0;
  int failed = 0;
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'dart', overrideMinimumMatches ?? 2000, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'java', overrideMinimumMatches ?? 39, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'h', overrideMinimumMatches ?? 30, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'm', overrideMinimumMatches ?? 30, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'swift', overrideMinimumMatches ?? 10, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'gradle', overrideMinimumMatches ?? 80, _generateLicense('// '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'gn', overrideMinimumMatches ?? 0, _generateLicense('# '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'sh', overrideMinimumMatches ?? 1, _generateLicense('# '), header: r'#!/usr/bin/env bash\n',);
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'bat', overrideMinimumMatches ?? 1, _generateLicense('REM '), header: r'@ECHO off\n');
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'ps1', overrideMinimumMatches ?? 1, _generateLicense('# '));
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'html', overrideMinimumMatches ?? 1, '<!-- ${_generateLicense('')} -->', trailingBlank: false, header: r'<!DOCTYPE HTML>\n');
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'xml', overrideMinimumMatches ?? 1, '<!-- ${_generateLicense('')} -->', header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?');
  failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'frag', overrideMinimumMatches ?? 1, _generateLicense('// '), header: r'#version 320 es(\n)+');
  if (failed > 0) {
    exitWithError(<String>['License check failed.']);
  }
}

Future<int> _verifyNoMissingLicenseForExtension(
  String workingDirectory,
  String extension,
  int minimumMatches,
  String license, {
  bool trailingBlank = true,
  // The "header" is a regular expression matching the header that comes before
  // the license in some files.
  String header = '',
}) async {
  assert(!license.endsWith('\n'));
  final String licensePattern = RegExp.escape('$license\n${trailingBlank ? '\n' : ''}');
  final List<String> errors = <String>[];
  await for (final File file in _allFiles(workingDirectory, extension, minimumMatches: minimumMatches)) {
    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(RegExp(header + licensePattern)))
      errors.add(file.path);
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
    final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
    final String fileDoes = errors.length == 1 ? 'file does' : '${errors.length} files do';
    print(<String>[
      redLine,
      '${bold}The following $fileDoes not have the right license header for $extension files:$reset',
      ...errors.map<String>((String error) => '  $error'),
      'The expected license header is:',
      if (header.isNotEmpty) 'A header matching the regular expression "$header",',
      if (header.isNotEmpty) 'followed by the following license text:',
      license,
      if (trailingBlank) '...followed by a blank line.',
      redLine,
    ].join('\n'));
    return 1;
  }
  return 0;
}

class _Line {
  _Line(this.line, this.content);

  final int line;
  final String content;
}

Iterable<_Line> _getTestSkips(File file) {
  final ParseStringResult parseResult = parseFile(
    featureSet: _parsingFeatureSet(),
    path: file.absolute.path,
  );
  final _TestSkipLinesVisitor<CompilationUnit> visitor = _TestSkipLinesVisitor<CompilationUnit>(parseResult);
  visitor.visitCompilationUnit(parseResult.unit);
  return visitor.skips;
}

class _TestSkipLinesVisitor<T> extends RecursiveAstVisitor<T> {
  _TestSkipLinesVisitor(this.parseResult) : skips = <_Line>{};

  final ParseStringResult parseResult;
  final Set<_Line> skips;

  static bool isTestMethod(String name) {
    return name.startsWith('test') || name == 'group' || name == 'expect';
  }

  @override
  T? visitMethodInvocation(MethodInvocation node) {
    if (isTestMethod(node.methodName.toString())) {
      for (final Expression argument in node.argumentList.arguments) {
        if (argument is NamedExpression && argument.name.label.name == 'skip') {
          skips.add(_getLine(parseResult, argument.beginToken.charOffset));
        }
      }
    }
    return super.visitMethodInvocation(node);
  }
}

final RegExp _skipTestCommentPattern = RegExp(r'//(.*)$');
const Pattern _skipTestIntentionalPattern = '[intended]';
final Pattern _skipTestTrackingBugPattern = RegExp(r'https+?://github.com/.*/issues/\d+');

Future<void> verifySkipTestComments(String workingDirectory) async {
  final List<String> errors = <String>[];
  final Stream<File> testFiles =_allFiles(workingDirectory, 'dart', minimumMatches: 1500)
    .where((File f) => f.path.endsWith('_test.dart'));

  await for (final File file in testFiles) {
    for (final _Line skip in _getTestSkips(file)) {
      final Match? match = _skipTestCommentPattern.firstMatch(skip.content);
      final String? skipComment = match?.group(1);
      if (skipComment == null ||
          !(skipComment.contains(_skipTestIntentionalPattern) ||
            skipComment.contains(_skipTestTrackingBugPattern))) {
        errors.add('${file.path}:${skip.line}: skip test without a justification comment.');
      }
    }
  }

  // Fail if any errors
  if (errors.isNotEmpty) {
    exitWithError(<String>[
      ...errors,
      '\n${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#skipped-tests$reset',
    ]);
  }
}

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',
  'package:integration_test/integration_test.dart',
};

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

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(<String>[
      'flutter/lib/*.dart does not match flutter/lib/src/*/:',
      'These are the exported packages:',
      ...packages.map<String>((String path) => '  lib/$path.dart'),
      'These are the directories:',
      ...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>>{};
  for (final String directory in directories) {
    dependencyMap[directory] = await _findFlutterDependencies(path.join(srcPath, directory), errors, checkForMeta: directory != 'foundation');
  }
  assert(dependencyMap['material']!.contains('widgets') &&
         dependencyMap['widgets']!.contains('rendering') &&
         dependencyMap['rendering']!.contains('painting')); // to make sure we're convinced _findFlutterDependencies is finding some
  for (final String package in dependencyMap.keys) {
    if (dependencyMap[package]!.contains(package)) {
      errors.add(
        'One of the files in the $yellow$package$reset package imports that package recursively.'
      );
    }
  }

  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.'
      );
    }
  }

  for (final String package in dependencyMap.keys) {
    final List<String>? loop = _deepSearch<String>(dependencyMap, package);
    if (loop != null) {
      errors.add('${yellow}Dependency loop:$reset ${loop.join(' depends on ')}');
    }
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
    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,
    ]);
  }
}

Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async {
  final List<String> errors = <String>[];
  final List<File> files = await _allFiles(path.join(workingDirectory, 'packages', 'flutter_tools', 'lib'), 'dart', minimumMatches: 200).toList();
  for (final File file in files) {
    if (file.readAsStringSync().contains('package:flutter_tools/')) {
      errors.add('$yellow${file.path}$reset imports flutter_tools.');
    }
  }
  // Fail if any errors
  if (errors.isNotEmpty) {
    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'),
    ]);
  }
}

Future<void> verifyIntegrationTestTimeouts(String workingDirectory) async {
  final List<String> errors = <String>[];
  final String dev = path.join(workingDirectory, 'dev');
  final List<File> files = await _allFiles(dev, 'dart', minimumMatches: 1)
      .where((File file) => file.path.contains('test_driver') && (file.path.endsWith('_test.dart') || file.path.endsWith('util.dart')))
      .toList();
  for (final File file in files) {
    final String contents = file.readAsStringSync();
    final int testCount = ' test('.allMatches(contents).length;
    final int timeoutNoneCount = 'timeout: Timeout.none'.allMatches(contents).length;
    if (testCount != timeoutNoneCount) {
      errors.add('$yellow${file.path}$reset has at least $testCount test(s) but only $timeoutNoneCount `Timeout.none`(s).');
    }
  }
  if (errors.isNotEmpty) {
    exitWithError(<String>[
      if (errors.length == 1)
        '${bold}An error was detected when looking at integration test timeouts:$reset'
      else
        '${bold}Multiple errors were detected when looking at integration test timeouts:$reset',
      ...errors.map((String paragraph) => '$paragraph\n'),
    ]);
  }
}

Future<void> verifyInternationalizations(String workingDirectory, String dartExecutable) async {
  final EvalResult materialGenResult = await _evalCommand(
    dartExecutable,
    <String>[
      path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
      '--material',
      '--remove-undefined',
    ],
    workingDirectory: workingDirectory,
  );
  final EvalResult cupertinoGenResult = await _evalCommand(
    dartExecutable,
    <String>[
      path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
      '--cupertino',
      '--remove-undefined',
    ],
    workingDirectory: workingDirectory,
  );

  final String materialLocalizationsFile = path.join(workingDirectory, 'packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_material_localizations.dart');
  final String cupertinoLocalizationsFile = path.join(workingDirectory, '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()) {
    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?',
    ]);
  }
  if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) {
    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?',
    ]);
  }
}


/// Verifies that all instances of "checked mode" have been migrated to "debug mode".
Future<void> verifyNoCheckedMode(String workingDirectory) async {
  final String flutterPackages = path.join(workingDirectory, 'packages');
  final List<File> files = await _allFiles(flutterPackages, 'dart', minimumMatches: 400)
      .where((File file) => path.extension(file.path) == '.dart')
      .toList();
  final List<String> problems = <String>[];
  for (final File file in files) {
    int lineCount = 0;
    for (final String line in file.readAsLinesSync()) {
      if (line.toLowerCase().contains('checked mode')) {
        problems.add('${file.path}:$lineCount uses deprecated "checked mode" instead of "debug mode".');
      }
      lineCount += 1;
    }
  }
  if (problems.isNotEmpty) {
    exitWithError(problems);
  }
}


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.
  };
  final List<File> files = await _allFiles(flutterLib, 'dart', minimumMatches: 400)
      .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);
}

Future<void> verifyNoTrailingSpaces(String workingDirectory, { int minimumMatches = 4000 }) async {
  final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches)
    .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')
    .where((File file) => path.extension(file.path) != '.ico')
    .where((File file) => path.extension(file.path) != '.jar')
    .where((File file) => path.extension(file.path) != '.swp')
    .toList();
  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 += 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);
}

String _bullets(String value) => ' * $value';

Future<void> verifyIssueLinks(String workingDirectory) async {
  const String issueLinkPrefix = 'https://github.com/flutter/flutter/issues/new';
  const Set<String> stops = <String>{ '\n', ' ', "'", '"', r'\', ')', '>' };
  assert(!stops.contains('.')); // instead of "visit https://foo." say "visit: https://", it copy-pastes better
  const String kGiveTemplates =
    'Prefer to provide a link either to $issueLinkPrefix/choose (the list of issue '
    'templates) or to a specific template directly ($issueLinkPrefix?template=...).\n';
  final Set<String> templateNames =
    Directory(path.join(workingDirectory, '.github', 'ISSUE_TEMPLATE'))
      .listSync()
      .whereType<File>()
      .where((File file) => path.extension(file.path) == '.md')
      .map<String>((File file) => path.basename(file.path))
      .toSet();
  final String kTemplates = 'The available templates are:\n${templateNames.map(_bullets).join("\n")}';
  final List<String> problems = <String>[];
  final Set<String> suggestions = <String>{};
  final List<File> files = await _gitFiles(workingDirectory);
  for (final File file in files) {
    if (path.basename(file.path).endsWith('_test.dart') || path.basename(file.path) == 'analyze.dart')
      continue; // Skip tests, they're not public-facing.
    final Uint8List bytes = file.readAsBytesSync();
    // We allow invalid UTF-8 here so that binaries don't trip us up.
    // There's a separate test in this file that verifies that all text
    // files are actually valid UTF-8 (see verifyNoBinaries below).
    final String contents = utf8.decode(bytes, allowMalformed: true);
    int start = 0;
    while ((start = contents.indexOf(issueLinkPrefix, start)) >= 0) {
      int end = start + issueLinkPrefix.length;
      while (end < contents.length && !stops.contains(contents[end])) {
        end += 1;
      }
      final String url = contents.substring(start, end);
      if (url == issueLinkPrefix) {
        if (file.path != path.join(workingDirectory, 'dev', 'bots', 'analyze.dart')) {
          problems.add('${file.path} contains a direct link to $issueLinkPrefix.');
          suggestions.add(kGiveTemplates);
          suggestions.add(kTemplates);
        }
      } else if (url.startsWith('$issueLinkPrefix?')) {
        final Uri parsedUrl = Uri.parse(url);
        final List<String>? templates = parsedUrl.queryParametersAll['template'];
        if (templates == null) {
          problems.add('${file.path} contains $url, which has no "template" argument specified.');
          suggestions.add(kGiveTemplates);
          suggestions.add(kTemplates);
        } else if (templates.length != 1) {
          problems.add('${file.path} contains $url, which has ${templates.length} templates specified.');
          suggestions.add(kGiveTemplates);
          suggestions.add(kTemplates);
        } else if (!templateNames.contains(templates.single)) {
          problems.add('${file.path} contains $url, which specifies a non-existent template ("${templates.single}").');
          suggestions.add(kTemplates);
        } else if (parsedUrl.queryParametersAll.keys.length > 1) {
          problems.add('${file.path} contains $url, which the analyze.dart script is not sure how to handle.');
          suggestions.add('Update analyze.dart to handle the URLs above, or change them to the expected pattern.');
        }
      } else if (url != '$issueLinkPrefix/choose') {
        problems.add('${file.path} contains $url, which the analyze.dart script is not sure how to handle.');
        suggestions.add('Update analyze.dart to handle the URLs above, or change them to the expected pattern.');
      }
      start = end;
    }
  }
  assert(problems.isEmpty == suggestions.isEmpty);
  if (problems.isNotEmpty) {
    exitWithError(<String>[
      ...problems,
      ...suggestions,
    ]);
  }
}

@immutable
class Hash256 {
  const Hash256(this.a, this.b, this.c, this.d);

  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 => Object.hash(a, b, c, d);
}

// DO NOT ADD ANY ENTRIES TO THIS LIST.
// We have a policy of not checking in binaries into this repository.
// 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.
final Set<Hash256> _legacyBinaries = <Hash256>{
  // 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)
  const Hash256(0x6A7C8F0D703E3682, 0x108F9662F8133022, 0x36240D3F8F638BB3, 0x91E32BFB96055FEF),

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

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

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

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

  // 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)
  const Hash256(0x7770183009E91411, 0x2DE7D8EF1D235A6A, 0x30C5834424858E0D, 0x2F8253F6B8D31926),

  // 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)
  const Hash256(0x5925DAB509451F9E, 0xCBB12CE8A625F9D4, 0xC104718EE20CAFF8, 0xB1B51032D1CD8946),

  // 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)
  const Hash256(0xC4D9A284C12301D0, 0xF50E248EC53ED51A, 0x19A10147B774B233, 0x08399250B0D44C55),

  // 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)
  const Hash256(0xBF97F9D3233F33E1, 0x389B09F7B8ADD537, 0x41300CB834D6C7A5, 0xCA32CBED363A4FB2),

  // 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)
  const Hash256(0x285442F69A06B45D, 0x9D79DF80321815B5, 0x46473548A37B7881, 0x9B68959C7B8ED237),

  // 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)
  const Hash256(0x2AB64AF8AC727EA9, 0x9C6AB9EAFF847F46, 0xFBF2A9A0A78A0ABC, 0xBF3180F3851645B4),

  // 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)
  const Hash256(0x9DCA09F4E5ED5684, 0xD3C4DFF41F4E8B7C, 0xB864B438172D72BE, 0x069315FA362930F9),

  // 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)
  const Hash256(0xD5AD04DE321EF37C, 0xACC5A7B960AFCCE7, 0x1BDCB96FA020C482, 0x49C1545DD1A0F497),

  // 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)
  const Hash256(0x809ABFE75C440770, 0xC13C4E2E46D09603, 0xC22053E9D4E0E227, 0x5DCB9C1DCFBB2C75),

  // 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)
  const Hash256(0x3DB08CB79E7B01B9, 0xE81F956E3A0AE101, 0x48D0FAFDE3EA7AA7, 0x0048DF905AA52CFD),

  // 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)
  const Hash256(0x23C13D463F5DCA5C, 0x1F14A14934003601, 0xC29F1218FD461016, 0xD8A22CEF579A665F),

  // 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)
  const Hash256(0x6DB7726530D71D3F, 0x52CB59793EB69131, 0x3BAA04796E129E1E, 0x043C0A58A1BFFD2F),

  // 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)
  const Hash256(0xCEE565F5E6211656, 0x9B64980B209FD5CA, 0x4B3D3739011F5343, 0x250B33A1A2C6EB65),

  // 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)
  const Hash256(0x93AE7D494FAD0FB3, 0x0CBF3AE746A39C4B, 0xC7A0F8BBF87FBB58, 0x7A3F3C01F3C5CE20),

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

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

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

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

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

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

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

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

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

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

  // GALLERY ICONS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


  // STOCKS ICONS

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

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

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

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

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

  // 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
  const Hash256(0x5BA3283A76918FC0, 0xEE127D0F22D7A0B6, 0xDF03DAED61669427, 0x93D89DDD87A08117),

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

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

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

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

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

  // 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
  const Hash256(0x743056FE7D83FE42, 0xA2990825B6AD0415, 0x1AF73D0D43B227AA, 0x07EBEA9B767381D9),

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

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

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

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

  // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
  const Hash256(0x4D9F5E81F668DA44, 0xB20A77F8BF7BA2E1, 0xF384533B5AD58F07, 0xB3A2F93F8635CD96),


  // 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)
  const Hash256(0x6E645DC9ED913AAD, 0xB50ED29EEB16830D, 0xB32CA12F39121DB9, 0xB7BC1449DDDBF8B8),

  // 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
  const Hash256(0xDEFAC77E08EC71EC, 0xA04CCA3C95D1FC33, 0xB9F26E1CB15CB051, 0x47DEFC79CDD7C158),

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

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

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


  // TEST ASSETS

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

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

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

  // 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
  const Hash256(0x2D4F8D7A3DFEF9D3, 0xA0C66938E169AB58, 0x8C6BBBBD1973E34E, 0x03C428416D010182),

  // 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
  const Hash256(0xCD46C01BAFA3B243, 0xA6AA1645EEDDE481, 0x143AC8ABAB1A0996, 0x22CAA9D41F74649A),

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

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

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

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

  // packages/flutter_tools/test/data/intellij/plugins/flutter-intellij.jar
  const Hash256(0x4C67221E25626CB2, 0x3F94E1F49D34E4CF, 0x3A9787A514924FC5, 0x9EF1E143E5BC5690),


  // MISCELLANEOUS

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

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

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

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

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

Future<void> verifyNoBinaries(String workingDirectory, { Set<Hash256>? legacyBinaries }) async {
  // Please do not add anything to the _legacyBinaries set above.
  // We have a policy of not checking in binaries into this repository.
  // 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.
  assert(
    _legacyBinaries
      .expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d])
      .reduce((int value, int element) => value ^ element) == 0x606B51C908B40BFA // Please do not modify this line.
  );
  legacyBinaries ??= _legacyBinaries;
  if (!Platform.isWindows) { // TODO(ianh): Port this to Windows
    final List<File> files = await _gitFiles(workingDirectory);
    final List<String> problems = <String>[];
    for (final File file in files) {
      final Uint8List bytes = file.readAsBytesSync();
      try {
        utf8.decode(bytes);
      } on FormatException catch (error) {
        final Digest digest = sha256.convert(bytes);
        if (!legacyBinaries.contains(Hash256.fromDigest(digest)))
          problems.add('${file.path}:${error.offset}: file is not valid UTF-8');
      }
    }
    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.',
        'To add assets to flutter_tools templates, see the instructions in the wiki:',
        'https://github.com/flutter/flutter/wiki/Managing-template-image-assets',
      ]);
    }
  }
}


// UTILITY FUNCTIONS

bool _listEquals<T>(List<T> a, List<T> b) {
  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;
}

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-files 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)));

  assert(extension == null || !extension.startsWith('.'), 'Extension argument should not start with a period.');
  final Set<FileSystemEntity> pending = <FileSystemEntity>{ Directory(workingDirectory) };
  int matches = 0;
  while (pending.isNotEmpty) {
    final FileSystemEntity entity = pending.first;
    pending.remove(entity);
    if (path.extension(entity.path) == '.tmpl')
      continue;
    if (entity is File) {
      if (!gitFileNamesSet.contains(path.canonicalize(entity.absolute.path)))
        continue;
      if (_isGeneratedPluginRegistrant(entity))
        continue;
      if (path.basename(entity.path) == 'flutter_export_environment.sh')
        continue;
      if (path.basename(entity.path) == 'gradlew.bat')
        continue;
      if (path.basename(entity.path) == '.DS_Store')
        continue;
      if (extension == null || path.extension(entity.path) == '.$extension') {
        matches += 1;
        yield entity;
      }
    } else if (entity is Directory) {
      if (File(path.join(entity.path, '.dartignore')).existsSync())
        continue;
      if (path.basename(entity.path) == '.git')
        continue;
      if (path.basename(entity.path) == '.idea')
        continue;
      if (path.basename(entity.path) == '.gradle')
        continue;
      if (path.basename(entity.path) == '.dart_tool')
        continue;
      if (path.basename(entity.path) == '.idea')
        continue;
      if (path.basename(entity.path) == 'build')
        continue;
      pending.addAll(entity.listSync());
    }
  }
  assert(matches >= minimumMatches, 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.');
}

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

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

// TODO(ianh): Refactor this to reuse the code in run_command.dart
Future<EvalResult> _evalCommand(String executable, List<String> arguments, {
  required String workingDirectory,
  Map<String, String>? environment,
  bool allowNonZeroExit = false,
  bool runSilently = false,
}) async {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);

  if (!runSilently) {
    printProgress('RUNNING', relativeWorkingDir, commandDescription);
  }

  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,
  );

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

  if (exitCode != 0 && !allowNonZeroExit) {
    stderr.write(result.stderr);
    exitWithError(<String>[
      '${bold}ERROR:$red Last command exited with $exitCode.$reset',
      '${bold}Command:$red $commandDescription$reset',
      '${bold}Relative working directory:$red $relativeWorkingDir$reset',
    ]);
  }

  return result;
}

Future<void> _checkConsumerDependencies() async {
  const List<String> kCorePackages = <String>[
    'flutter',
    'flutter_test',
    'flutter_driver',
    'flutter_localizations',
    'integration_test',
    'fuchsia_remote_debug_protocol',
  ];
  final Set<String> dependencies = <String>{};

  // Parse the output of pub deps --json to determine all of the
  // current packages used by the core set of flutter packages.
  for (final String package in kCorePackages) {
    final ProcessResult result = await Process.run(flutter, <String>[
      'pub',
      'deps',
      '--json',
      '--directory=${path.join(flutterRoot, 'packages', package)}',
    ]);
    if (result.exitCode != 0) {
      print(result.stdout as Object);
      print(result.stderr as Object);
      exit(result.exitCode);
    }
    final Map<String, Object?> rawJson = json.decode(result.stdout as String) as Map<String, Object?>;
    final Map<String, Map<String, Object?>> dependencyTree = <String, Map<String, Object?>>{
      for (final Map<String, Object?> package in (rawJson['packages']! as List<Object?>).cast<Map<String, Object?>>())
        package['name']! as String : package,
    };
    final List<Map<String, Object?>> workset = <Map<String, Object?>>[];
    workset.add(dependencyTree[package]!);

    while (workset.isNotEmpty) {
      final Map<String, Object?> currentPackage = workset.removeLast();
      if (currentPackage['kind'] == 'dev') {
        continue;
      }
      dependencies.add(currentPackage['name']! as String);

      final List<String> currentDependencies = (currentPackage['dependencies']! as List<Object?>).cast<String>();
      for (final String dependency in currentDependencies) {
        workset.add(dependencyTree[dependency]!);
      }
    }
  }

  final Set<String> removed = kCorePackageAllowList.difference(dependencies);
  final Set<String> added = dependencies.difference(kCorePackageAllowList);

  String plural(int n, String s, String p) => n == 1 ? s : p;

  if (added.isNotEmpty) {
    exitWithError(<String>[
      'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:',
      '  ${added.join(', ')}',
      'We strongly desire to keep the number of dependencies to a minimum and',
      'therefore would much prefer not to add new dependencies.',
      'See dev/bots/allowlist.dart for instructions on how to update the package',
      'allowlist if you nonetheless believe this is a necessary addition.',
    ]);
  }

  if (removed.isNotEmpty) {
    exitWithError(<String>[
      'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}',
      '  ${removed.join(', ')}',
      'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,',
      'please remove ${plural(removed.length, "this", "these")} packages from the allow-list in dev/bots/allowlist.dart.',
      'Thanks!',
    ]);
  }
}

const String _kDebugOnlyAnnotation = '@_debugOnly';
final RegExp _nullInitializedField = RegExp(r'kDebugMode \? [\w<> ,{}()]+ : null;');

Future<void> verifyNullInitializedDebugExpensiveFields(String workingDirectory, {int minimumMatches = 400}) async {
  final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
  final List<File> files = await _allFiles(flutterLib, 'dart', minimumMatches: minimumMatches)
    .toList();
  final List<String> errors = <String>[];
  for (final File file in files) {
    final List<String> lines = file.readAsLinesSync();
    for (int i = 0; i < lines.length; i += 1) {
      final String line = lines[i];
      if (!line.contains(_kDebugOnlyAnnotation)) {
        continue;
      }
      final String nextLine = lines[i + 1];
      if (_nullInitializedField.firstMatch(nextLine) == null) {
        errors.add('${file.path} L$i');
      }
    }
  }

  if (errors.isNotEmpty) {
    exitWithError(<String>[
     '${bold}ERROR: ${red}fields annotated with @_debugOnly must null initialize.$reset',
     'to ensure both the field and initializer are removed from profile/release mode.',
     'These fields should be written as:\n',
     'field = kDebugMode ? <DebugValue> : null;\n',
     'Errors were found in the following files:',
      ...errors,
    ]);
  }
}

Future<CommandResult> _runFlutterAnalyze(String workingDirectory, {
  List<String> options = const <String>[],
}) async {
  return runCommand(
    flutter,
    <String>['analyze', ...options],
    workingDirectory: workingDirectory,
  );
}

// These files legitimately require executable permissions
const Set<String> kExecutableAllowlist = <String>{
  'bin/dart',
  'bin/flutter',
  'bin/internal/update_dart_sdk.sh',

  'dev/bots/accept_android_sdk_licenses.sh',
  'dev/bots/codelabs_build_test.sh',
  'dev/bots/docs.sh',

  'dev/conductor/bin/conductor',
  'dev/conductor/core/lib/src/proto/compile_proto.sh',

  'dev/customer_testing/ci.sh',

  'dev/integration_tests/flutter_gallery/tool/run_instrumentation_test.sh',

  'dev/integration_tests/ios_add2app_life_cycle/build_and_test.sh',

  'dev/integration_tests/deferred_components_test/download_assets.sh',
  'dev/integration_tests/deferred_components_test/run_release_test.sh',

  'dev/tools/gen_keycodes/bin/gen_keycodes',
  'dev/tools/repackage_gradle_wrapper.sh',

  'packages/flutter_tools/bin/macos_assemble.sh',
  'packages/flutter_tools/bin/tool_backend.sh',
  'packages/flutter_tools/bin/xcode_backend.sh',
};

Future<void> _checkForNewExecutables() async {
  // 0b001001001
  const int executableBitMask = 0x49;

  final List<File> files = await _gitFiles(flutterRoot);
  int unexpectedExecutableCount = 0;
  for (final File file in files) {
    final String relativePath = path.relative(
      file.path,
      from: flutterRoot,
    );
    final FileStat stat = file.statSync();
    final bool isExecutable = stat.mode & executableBitMask != 0x0;
    if (isExecutable && !kExecutableAllowlist.contains(relativePath)) {
      unexpectedExecutableCount += 1;
      print('$relativePath is executable: ${(stat.mode & 0x1FF).toRadixString(2)}');
    }
  }
  if (unexpectedExecutableCount > 0) {
    throw Exception(
      'found $unexpectedExecutableCount unexpected executable file'
      '${unexpectedExecutableCount == 1 ? '' : 's'}! If this was intended, you '
      'must add this file to kExecutableAllowlist in dev/bots/analyze.dart',
    );
  }
}

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

Future<Set<String>> _findFlutterDependencies(String srcPath, List<String> errors, { bool checkForMeta = false }) async {
  return _allFiles(srcPath, 'dart', minimumMatches: 1)
    .map<Set<String>>((File file) {
      final Set<String> result = <String>{};
      for (final String line in file.readAsLinesSync()) {
        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.'
            );
          }
        }
      }
      return result;
    })
    .reduce((Set<String>? value, Set<String> element) {
      value ??= <String>{};
      value.addAll(element);
      return value;
    });
}

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

  for (final T key in map[start]!) {
    if (key == start)
      continue; // we catch these separately
    if (seen != null && seen.contains(key))
      return <T>[start, key];
    final List<T>? result = _deepSearch<T>(
      map,
      key,
      <T>{
        if (seen == null) start else ...seen,
        key,
      },
    );
    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' ||
          filename == 'GeneratedPluginRegistrant.m' ||
          filename == 'generated_plugin_registrant.dart' ||
          filename == 'generated_plugin_registrant.h');
}
