// 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 main(List args) async { final Options options = Options.fromArgs(args); final String json = options.snapshot.readAsStringSync(); final Snapshot snapshot = Snapshot.fromJson(jsonDecode(json) as Map); final ProgramInfo programInfo = toProgramInfo(snapshot); final List foundForbiddenTypes = []; 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 ::, 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 lookupPath = [ 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 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 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 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: '::', ); 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.from(argResults['forbidden-type'] as List), ); } final File snapshot; final File packageConfig; final Set 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; } }