| // 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:vm_snapshot_analysis/program_info.dart'; | |
| import 'package:vm_snapshot_analysis/v8_profile.dart'; | |
| 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, | |
| ); | |
| final Uri? packageFileUri = packageConfig.resolve(packageUri); | |
| 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({ | |
| required this.snapshot, | |
| required this.packageConfig, | |
| required this.forbiddenTypes, | |
| }); | |
| 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; | |
| } | |
| } |