| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| // @dart = 2.9 |
| |
| import 'dart:convert' show utf8; |
| |
| import 'dart:io' |
| show Directory, File, FileSystemEntity, exitCode, stdin, stdout; |
| |
| import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity; |
| |
| import 'package:_fe_analyzer_shared/src/scanner/token.dart' |
| show CommentToken, Token; |
| |
| import 'package:front_end/src/api_prototype/compiler_options.dart' as api |
| show CompilerOptions, DiagnosticMessage; |
| |
| import 'package:front_end/src/api_prototype/file_system.dart' as api |
| show FileSystem; |
| |
| import 'package:front_end/src/base/processed_options.dart' |
| show ProcessedOptions; |
| |
| import 'package:front_end/src/compute_platform_binaries_location.dart' |
| show computePlatformBinariesLocation; |
| |
| import 'package:front_end/src/fasta/compiler_context.dart' show CompilerContext; |
| |
| import 'package:front_end/src/fasta/dill/dill_target.dart' show DillTarget; |
| |
| import 'package:front_end/src/fasta/incremental_compiler.dart' |
| show IncrementalCompiler, IncrementalKernelTarget; |
| |
| import 'package:front_end/src/fasta/kernel/kernel_target.dart' |
| show KernelTarget; |
| |
| import 'package:front_end/src/fasta/source/source_library_builder.dart' |
| show SourceLibraryBuilder; |
| |
| import 'package:front_end/src/fasta/source/source_loader.dart' |
| show SourceLoader; |
| |
| import 'package:front_end/src/fasta/uri_translator.dart' show UriTranslator; |
| |
| import 'package:kernel/ast.dart'; |
| |
| import 'package:kernel/target/targets.dart' show TargetFlags; |
| |
| import "package:vm/target/vm.dart" show VmTarget; |
| |
| import "utils/io_utils.dart" show computeRepoDirUri; |
| |
| final Uri repoDir = computeRepoDirUri(); |
| |
| Set<Uri> libUris = {}; |
| |
| Component component; |
| |
| Future<void> main(List<String> args) async { |
| api.CompilerOptions compilerOptions = getOptions(); |
| |
| Uri dotPackagesUri = repoDir.resolve(".packages"); |
| if (!new File.fromUri(dotPackagesUri).existsSync()) { |
| throw "Couldn't find .packages"; |
| } |
| compilerOptions.packagesFileUri = dotPackagesUri; |
| |
| ProcessedOptions options = new ProcessedOptions(options: compilerOptions); |
| |
| libUris.add(repoDir.resolve("pkg/_fe_analyzer_shared/lib/src/parser")); |
| libUris.add(repoDir.resolve("pkg/_fe_analyzer_shared/lib/src/scanner")); |
| for (Uri uri in libUris) { |
| List<FileSystemEntity> entities = |
| new Directory.fromUri(uri).listSync(recursive: true); |
| for (FileSystemEntity entity in entities) { |
| if (entity is File && entity.path.endsWith(".dart")) { |
| options.inputs.add(entity.uri); |
| } |
| } |
| } |
| CompilerContext context = new CompilerContext(options); |
| IncrementalCompiler incrementalCompiler = |
| new TestIncrementalCompiler(context); |
| component = await incrementalCompiler.computeDelta(); |
| |
| for (Library library in component.libraries) { |
| if (library.importUri.scheme == "dart") continue; |
| // This isn't perfect because of parts, but (for now) it'll do. |
| for (Uri uri in libUris) { |
| if (library.fileUri.toString().startsWith(uri.toString())) { |
| library.accept(new InvocationVisitor()); |
| break; |
| } |
| } |
| } |
| |
| if (args.isNotEmpty && args[0] == "--interactive") { |
| List<Uri> editsPerformed = []; |
| for (Uri uri in edits.keys) { |
| print("\n\n\n"); |
| if (edits[uri] != null && edits[uri].isNotEmpty) { |
| String update; |
| while (update != "y" && |
| update != "yes" && |
| update != "n" && |
| update != "no") { |
| print("Do you want to update $uri? (y/n)"); |
| update = stdin.readLineSync(); |
| } |
| if (update != "y" && update != "yes") continue; |
| |
| List<Edit> theseEdits = edits[uri]; |
| theseEdits.sort((a, b) => a.offset - b.offset); |
| String content = utf8.decode(component.uriToSource[uri].source, |
| allowMalformed: true); |
| StringBuffer sb = new StringBuffer(); |
| int latest = 0; |
| for (Edit edit in theseEdits) { |
| sb.write(content.substring(latest, edit.offset)); |
| sb.write(edit.insertData); |
| latest = edit.offset; |
| } |
| sb.write(content.substring(latest, content.length)); |
| new File.fromUri(uri).writeAsStringSync(sb.toString()); |
| editsPerformed.add(uri); |
| } |
| } |
| if (editsPerformed.isNotEmpty) { |
| print("\n\nYou should now probably run something like\n\n"); |
| stdout.write(r"tools/sdks/dart-sdk/bin/dartfmt -w"); |
| for (Uri uri in editsPerformed) { |
| File f = new File.fromUri(uri); |
| Directory relative = new Directory.fromUri(Uri.base.resolve(".")); |
| if (!f.path.startsWith(relative.path)) { |
| throw "${f.path} vs ${relative.path}"; |
| } |
| String relativePath = f.path.substring(relative.path.length); |
| stdout.write(" ${relativePath}"); |
| } |
| stdout.write("\n\n"); |
| } |
| } |
| |
| if (edits.isNotEmpty) { |
| exitCode = 1; |
| } |
| |
| int totalSuggestedEdits = edits.values |
| .fold(0, (previousValue, element) => previousValue + element.length); |
| print("Done. Suggested ${totalSuggestedEdits} edits " |
| "in ${edits.length} files."); |
| } |
| |
| int errorCount = 0; |
| |
| api.CompilerOptions getOptions() { |
| // Compile sdk because when this is run from a lint it uses the checked-in sdk |
| // and we might not have a suitable compiled platform.dill file. |
| Uri sdkRoot = computePlatformBinariesLocation(forceBuildDir: true); |
| api.CompilerOptions options = new api.CompilerOptions() |
| ..sdkRoot = sdkRoot |
| ..compileSdk = true |
| ..target = new VmTarget(new TargetFlags()) |
| ..librariesSpecificationUri = repoDir.resolve("sdk/lib/libraries.json") |
| ..omitPlatform = true |
| ..onDiagnostic = (api.DiagnosticMessage message) { |
| if (message.severity == Severity.error) { |
| print(message.plainTextFormatted.join('\n')); |
| errorCount++; |
| } |
| } |
| ..environmentDefines = const {}; |
| return options; |
| } |
| |
| class InvocationVisitor extends RecursiveVisitor { |
| void visitProcedure(Procedure node) { |
| if (node.isNoSuchMethodForwarder) return; |
| super.visitProcedure(node); |
| } |
| |
| void visitSuperMethodInvocation(SuperMethodInvocation node) { |
| super.visitSuperMethodInvocation(node); |
| note(node.interfaceTargetReference.node, node.arguments, node); |
| } |
| |
| void visitStaticInvocation(StaticInvocation node) { |
| super.visitStaticInvocation(node); |
| note(node.targetReference.node, node.arguments, node); |
| } |
| |
| void visitConstructorInvocation(ConstructorInvocation node) { |
| super.visitConstructorInvocation(node); |
| note(node.targetReference.node, node.arguments, node); |
| } |
| |
| void note( |
| NamedNode node, Arguments arguments, InvocationExpression invocation) { |
| List<VariableDeclaration> positionalParameters; |
| if (node is Procedure) { |
| positionalParameters = node.function.positionalParameters; |
| } else if (node is Constructor) { |
| positionalParameters = node.function.positionalParameters; |
| } else { |
| throw "Unexpected node: ${node.runtimeType}"; |
| } |
| |
| for (int i = 0; i < arguments.positional.length; i++) { |
| bool wantComment = false; |
| if (arguments.positional[i] is NullLiteral || |
| arguments.positional[i] is BoolLiteral || |
| arguments.positional[i] is IntLiteral) { |
| wantComment = true; |
| } else if (arguments.positional[i] is MapLiteral) { |
| MapLiteral literal = arguments.positional[i]; |
| if (literal.entries.isEmpty) wantComment = true; |
| } else if (arguments.positional[i] is ListLiteral) { |
| ListLiteral literal = arguments.positional[i]; |
| if (literal.expressions.isEmpty) wantComment = true; |
| } else if (arguments.positional[i] is InstanceInvocation) { |
| InstanceInvocation methodInvocation = arguments.positional[i]; |
| if (methodInvocation.receiver is NullLiteral || |
| methodInvocation.receiver is IntLiteral || |
| methodInvocation.receiver is BoolLiteral) { |
| wantComment = true; |
| } |
| } else if (arguments.positional[i] is DynamicInvocation) { |
| DynamicInvocation methodInvocation = arguments.positional[i]; |
| if (methodInvocation.receiver is NullLiteral || |
| methodInvocation.receiver is IntLiteral || |
| methodInvocation.receiver is BoolLiteral) { |
| wantComment = true; |
| } |
| } |
| if (wantComment) { |
| check(arguments.positional[i], i, positionalParameters[i], node, |
| "/* ${positionalParameters[i].name} = */"); |
| } |
| } |
| } |
| } |
| |
| Map<Uri, Token> cache = {}; |
| |
| void check( |
| Expression argumentExpression, |
| int parameterNumber, |
| VariableDeclaration parameter, |
| NamedNode targetNode, |
| String expectedComment) { |
| if (targetNode is Procedure && targetNode.kind == ProcedureKind.Operator) { |
| // Operator calls doesn't look like 'regular' method calls. |
| return; |
| } |
| if (argumentExpression.fileOffset == -1) return; |
| Location location = argumentExpression.location; |
| Token token = cache[location.file]; |
| while (token.offset != argumentExpression.fileOffset) { |
| token = token.next; |
| if (token.isEof) { |
| throw "Couldn't find token for $argumentExpression " |
| "(${argumentExpression.fileOffset})."; |
| } |
| } |
| bool foundComment = false; |
| CommentToken commentToken = token.precedingComments; |
| while (commentToken != null) { |
| if (commentToken.lexeme == expectedComment) { |
| // Exact match. |
| foundComment = true; |
| break; |
| } |
| if (commentToken.lexeme.replaceAll(" ", "") == |
| expectedComment.replaceAll(" ", "")) { |
| // Close enough. |
| foundComment = true; |
| break; |
| } |
| commentToken = commentToken.next; |
| } |
| if (foundComment) { |
| return; |
| } |
| Location calculatedLocation = |
| component.getLocation(location.file, token.offset); |
| print("Please add comment $expectedComment at " |
| "${token.offset} => " |
| "${calculatedLocation}"); |
| edits[location.file] ??= []; |
| edits[location.file].add(new Edit(token.offset, expectedComment)); |
| } |
| |
| Map<Uri, List<Edit>> edits = {}; |
| |
| class Edit { |
| final int offset; |
| final String insertData; |
| Edit(this.offset, this.insertData); |
| } |
| |
| class TestIncrementalCompiler extends IncrementalCompiler { |
| TestIncrementalCompiler(CompilerContext context) : super(context); |
| |
| IncrementalKernelTarget createIncrementalKernelTarget( |
| api.FileSystem fileSystem, |
| bool includeComments, |
| DillTarget dillTarget, |
| UriTranslator uriTranslator) { |
| return new TestIncrementalKernelTarget( |
| fileSystem, /* includeComments = */ true, dillTarget, uriTranslator); |
| } |
| } |
| |
| class TestIncrementalKernelTarget extends IncrementalKernelTarget { |
| TestIncrementalKernelTarget(api.FileSystem fileSystem, bool includeComments, |
| DillTarget dillTarget, UriTranslator uriTranslator) |
| : super(fileSystem, includeComments, dillTarget, uriTranslator); |
| |
| SourceLoader createLoader() => |
| new TestSourceLoader(fileSystem, includeComments, this); |
| |
| void runBuildTransformations() { |
| // Don't do any transformations! |
| } |
| } |
| |
| class TestSourceLoader extends SourceLoader { |
| TestSourceLoader( |
| api.FileSystem fileSystem, bool includeComments, KernelTarget target) |
| : super(fileSystem, includeComments, target); |
| |
| Future<Token> tokenize(SourceLibraryBuilder library, |
| {bool suppressLexicalErrors: false}) async { |
| Token result = await super |
| .tokenize(library, suppressLexicalErrors: suppressLexicalErrors); |
| cache[library.fileUri] = result; |
| return result; |
| } |
| } |