blob: b8fd17e9404546a039d295c5c14577a0598a0b43 [file] [log] [blame]
// 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.
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/api_prototype/incremental_kernel_generator.dart'
show IncrementalCompilerResult;
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 = {};
late Component component;
Future<void> main(List<String> args) async {
api.CompilerOptions compilerOptions = getOptions();
Uri packageConfigUri = repoDir.resolve(".dart_tool/package_config.json");
if (!new File.fromUri(packageConfigUri).existsSync()) {
throw "Couldn't find .dart_tool/package_config.json";
}
compilerOptions.packagesFileUri = packageConfigUri;
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);
IncrementalCompilerResult incrementalCompilerResult =
await incrementalCompiler.computeDelta();
component = incrementalCompilerResult.component;
for (Library library in component.libraries) {
if (library.importUri.isScheme("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");
List<Edit>? theseEdits = edits[uri];
if (theseEdits != null && theseEdits.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;
theseEdits.sort();
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));
switch (edit.editType) {
case EditType.Insert:
print(edit);
sb.write(edit.insertData);
latest = edit.offset;
break;
case EditType.Delete:
print(edit);
// We "delete" by skipping...
latest = edit.offset + edit.length!;
break;
}
}
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("dart format");
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 {
@override
void visitProcedure(Procedure node) {
if (node.isNoSuchMethodForwarder) return;
super.visitProcedure(node);
}
@override
void visitSuperMethodInvocation(SuperMethodInvocation node) {
super.visitSuperMethodInvocation(node);
note(node.interfaceTargetReference.node!, node.arguments, node);
}
@override
void visitStaticInvocation(StaticInvocation node) {
super.visitStaticInvocation(node);
note(node.targetReference.node!, node.arguments, node);
}
@override
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;
Expression argument = arguments.positional[i];
if (argument is NullLiteral ||
argument is BoolLiteral ||
argument is IntLiteral) {
wantComment = true;
} else if (argument is MapLiteral) {
if (argument.entries.isEmpty) wantComment = true;
} else if (argument is ListLiteral) {
if (argument.expressions.isEmpty) wantComment = true;
} else if (argument is InstanceInvocation) {
if (argument.receiver is NullLiteral ||
argument.receiver is IntLiteral ||
argument.receiver is BoolLiteral) {
wantComment = true;
}
} else if (argument is DynamicInvocation) {
if (argument.receiver is NullLiteral ||
argument.receiver is IntLiteral ||
argument.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;
List<CommentToken> badComments = [];
CommentToken? commentToken = token.precedingComments;
while (commentToken != null) {
if (commentToken.lexeme == expectedComment) {
// Exact match.
foundComment = true;
break;
}
if (commentToken.lexeme.startsWith("/*") &&
commentToken.lexeme.endsWith("= */")) {
badComments.add(commentToken);
}
commentToken = commentToken.next as CommentToken?;
}
if (badComments.isNotEmpty) {
for (CommentToken comment in badComments) {
Location calculatedLocation =
component.getLocation(location.file, comment.offset)!;
print("Please remove comment of length ${comment.lexeme.length} at "
"${comment.offset} => "
"${calculatedLocation}");
(edits[location.file] ??= [])
.add(new Edit.delete(comment.offset, comment.lexeme.length));
}
}
if (foundComment) {
return;
}
Location calculatedLocation =
component.getLocation(location.file, token.offset)!;
print("Please add comment $expectedComment at "
"${token.offset} => "
"${calculatedLocation}");
(edits[location.file] ??= [])
.add(new Edit.insert(token.offset, expectedComment));
}
Map<Uri, List<Edit>> edits = {};
enum EditType { Insert, Delete }
class Edit implements Comparable<Edit> {
final int offset;
final int? length;
final String? insertData;
final EditType editType;
Edit.insert(this.offset, this.insertData)
: editType = EditType.Insert,
length = null;
Edit.delete(this.offset, this.length)
: editType = EditType.Delete,
insertData = null;
@override
int compareTo(Edit other) {
if (offset != other.offset) {
return offset - other.offset;
}
throw "Why did this happen?";
}
@override
String toString() {
return "Edit[$editType @ $offset]";
}
}
class TestIncrementalCompiler extends IncrementalCompiler {
TestIncrementalCompiler(CompilerContext context) : super(context);
@override
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);
@override
SourceLoader createLoader() =>
new TestSourceLoader(fileSystem, includeComments, this);
@override
void runBuildTransformations() {
// Don't do any transformations!
}
}
class TestSourceLoader extends SourceLoader {
TestSourceLoader(
api.FileSystem fileSystem, bool includeComments, KernelTarget target)
: super(fileSystem, includeComments, target);
@override
Future<Token> tokenize(SourceLibraryBuilder library,
{bool suppressLexicalErrors: false}) async {
Token result = await super
.tokenize(library, suppressLexicalErrors: suppressLexicalErrors);
cache[library.fileUri] = result;
return result;
}
}