main.dart 5.31 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
// 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:io';

import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:vm_snapshot_analysis/program_info.dart';
import 'package:vm_snapshot_analysis/v8_profile.dart';

const ProcessManager processManager = LocalProcessManager();
const FileSystem fs = LocalFileSystem();

Future<void> main(List<String> args) async {
  final Options options = Options.fromArgs(args);
  final String json = options.snapshot.readAsStringSync();
  final Snapshot snapshot = Snapshot.fromJson(jsonDecode(json) as Map<String, dynamic>);
  final ProgramInfo programInfo = toProgramInfo(snapshot);

  final List<String> foundForbiddenTypes = <String>[];
  bool fail = false;
  for (final String forbiddenType in options.forbiddenTypes) {
    final int slash = forbiddenType.indexOf('/');
    final int doubleColons = forbiddenType.indexOf('::');
    if (slash == -1 || doubleColons < 2) {
      print('Invalid forbidden type "$forbiddenType". The format must be <package_uri>::<type_name>, e.g. package:flutter/src/widgets/framework.dart::Widget');
      fail = true;
      continue;
    }

    if (!await validateType(forbiddenType, options.packageConfig)) {
      foundForbiddenTypes.add('Forbidden type "$forbiddenType" does not seem to exist.');
      continue;
    }

    final List<String> lookupPath = <String>[
      forbiddenType.substring(0, slash),
      forbiddenType.substring(0, doubleColons),
      forbiddenType.substring(doubleColons + 2),
    ];
    if (programInfo.lookup(lookupPath) != null) {
      foundForbiddenTypes.add(forbiddenType);
    }
  }
  if (fail) {
    print('Invalid forbidden type formats. Exiting.');
    exit(-1);
  }
  if (foundForbiddenTypes.isNotEmpty) {
    print('The output contained the following forbidden types:');
    print(foundForbiddenTypes.join('\n'));
    exit(-1);
  }

  print('No forbidden types found.');
}

Future<bool> validateType(String forbiddenType, File packageConfigFile) async {
  if (!forbiddenType.startsWith('package:')) {
    print('Warning: Unable to validate $forbiddenType. Continuing.');
    return true;
  }

  final Uri packageUri = Uri.parse(forbiddenType.substring(0, forbiddenType.indexOf('::')));
  final String typeName = forbiddenType.substring(forbiddenType.indexOf('::') + 2);

  final PackageConfig packageConfig = PackageConfig.parseString(
    packageConfigFile.readAsStringSync(),
    packageConfigFile.uri,
  );
77
  final Uri? packageFileUri = packageConfig.resolve(packageUri);
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  final File packageFile = fs.file(packageFileUri);
  if (!packageFile.existsSync()) {
    print('File $packageFile does not exist - forbidden type has moved or been removed.');
    return false;
  }

  // This logic is imperfect. It will not detect mixed in types the way that
  // the snapshot has them, e.g. TypeName&MixedIn&Whatever. It also assumes
  // there is at least one space before and after the type name, which is not
  // strictly required by the language.
  final List<String> contents = packageFile.readAsStringSync().split('\n');
  for (final String line in contents) {
    // Ignore comments.
    // This will fail for multi- and intra-line comments (i.e. /* */).
    if (line.trim().startsWith('//')) {
      continue;
    }
    if (line.contains(' $typeName ')) {
      return true;
    }
  }
  return false;
}

class Options {
  const Options({
104 105 106
    required this.snapshot,
    required this.packageConfig,
    required this.forbiddenTypes,
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
  });

  factory Options.fromArgs(List<String> args) {
    final ArgParser argParser = ArgParser();
    argParser.addOption(
      'snapshot',
      help: 'The path V8 snapshot file.',
      valueHelp: '/tmp/snapshot.arm64-v8a.json',
    );
    argParser.addOption(
      'package-config',
      help: 'Dart package_config.json file generated by `pub get`.',
      valueHelp: path.join(r'$FLUTTER_ROOT', 'examples', 'hello_world', '.dart_tool', 'package_config.json'),
      defaultsTo: path.join(fs.currentDirectory.path, 'examples', 'hello_world', '.dart_tool', 'package_config.json'),
    );
    argParser.addMultiOption(
      'forbidden-type',
      help: 'Type name(s) to forbid from release compilation, e.g. "package:flutter/src/widgets/framework.dart::Widget".',
      valueHelp: '<package_uri>::<type_name>',
    );

    argParser.addFlag('help', help: 'Prints usage.', negatable: false);
    final ArgResults argResults = argParser.parse(args);

    if (argResults['help'] == true) {
      print(argParser.usage);
      exit(0);
    }

    return Options(
      snapshot: _getFileArg(argResults, 'snapshot'),
      packageConfig: _getFileArg(argResults, 'package-config'),
      forbiddenTypes: Set<String>.from(argResults['forbidden-type'] as List<String>),
    );
  }

  final File snapshot;
  final File packageConfig;
  final Set<String> forbiddenTypes;

  static File _getFileArg(ArgResults argResults, String argName) {
    final File result = fs.file(argResults[argName] as String);
    if (!result.existsSync()) {
      print('The $argName file at $result could not be found.');
      exit(-1);
    }
    return result;
  }
}