blob: 6564bd1ef82ab083801b8ab0c7df8c75cad695f6 [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' as json;
import 'dart:io';
import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart';
import 'package:expect/expect.dart';
import 'package:front_end/src/api_prototype/compiler_options.dart';
import 'package:front_end/src/api_prototype/kernel_generator.dart';
import 'package:front_end/src/api_prototype/terminal_color_support.dart';
import 'package:front_end/src/compute_platform_binaries_location.dart';
import 'package:front_end/src/fasta/command_line_reporting.dart';
import 'package:front_end/src/fasta/fasta_codes.dart';
import 'package:front_end/src/fasta/kernel/redirecting_factory_body.dart';
import 'package:front_end/src/kernel_generator_impl.dart';
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/type_environment.dart';
Future<void> run(Uri entryPoint, String allowedListPath,
{bool verbose = false,
bool generate = false,
bool Function(Uri uri)? analyzedUrisFilter}) async {
CompilerOptions options = new CompilerOptions();
options.sdkRoot = computePlatformBinariesLocation(forceBuildDir: true);
options.onDiagnostic = (DiagnosticMessage message) {
printDiagnosticMessage(message, print);
};
InternalCompilerResult compilerResult = await kernelForProgramInternal(
entryPoint, options, retainDataForTesting: true, requireMain: false)
as InternalCompilerResult;
new DynamicVisitor(options.onDiagnostic!, compilerResult.component!,
allowedListPath, analyzedUrisFilter)
.run(verbose: verbose, generate: generate);
}
class StaticTypeVisitorBase extends RecursiveVisitor {
final TypeEnvironment typeEnvironment;
StaticTypeContext? staticTypeContext;
StaticTypeVisitorBase(Component component, ClassHierarchy classHierarchy)
: typeEnvironment =
new TypeEnvironment(new CoreTypes(component), classHierarchy);
@override
void visitProcedure(Procedure node) {
if (node.kind == ProcedureKind.Factory && isRedirectingFactory(node)) {
// Don't visit redirecting factories.
return;
}
staticTypeContext = new StaticTypeContext(node, typeEnvironment);
super.visitProcedure(node);
staticTypeContext = null;
}
@override
void visitField(Field node) {
if (isRedirectingFactoryField(node)) {
// Skip synthetic .dill members.
return;
}
staticTypeContext = new StaticTypeContext(node, typeEnvironment);
super.visitField(node);
staticTypeContext = null;
}
@override
void visitConstructor(Constructor node) {
staticTypeContext = new StaticTypeContext(node, typeEnvironment);
super.visitConstructor(node);
staticTypeContext = null;
}
}
class DynamicVisitor extends StaticTypeVisitorBase {
// TODO(johnniwinther): Enable this when it is less noisy.
static const bool checkReturnTypes = false;
final DiagnosticMessageHandler onDiagnostic;
final Component component;
final String? _allowedListPath;
final bool Function(Uri uri)? analyzedUrisFilter;
Map _expectedJson = {};
Map<String, Map<String, List<FormattedMessage>>> _actualMessages = {};
DynamicVisitor(this.onDiagnostic, this.component, this._allowedListPath,
this.analyzedUrisFilter)
: super(
component, new ClassHierarchy(component, new CoreTypes(component)));
void run({bool verbose = false, bool generate = false}) {
if (!generate && _allowedListPath != null) {
File file = new File(_allowedListPath!);
if (file.existsSync()) {
try {
_expectedJson = json.jsonDecode(file.readAsStringSync());
} catch (e) {
Expect.fail('Error reading allowed list from $_allowedListPath: $e');
}
}
}
component.accept(this);
if (generate && _allowedListPath != null) {
Map<String, Map<String, int>> actualJson = {};
_actualMessages.forEach(
(String uri, Map<String, List<FormattedMessage>> actualMessagesMap) {
Map<String, int> map = {};
actualMessagesMap
.forEach((String message, List<FormattedMessage> actualMessages) {
map[message] = actualMessages.length;
});
actualJson[uri] = map;
});
new File(_allowedListPath!).writeAsStringSync(
new json.JsonEncoder.withIndent(' ').convert(actualJson));
return;
}
int errorCount = 0;
_expectedJson.forEach((uri, expectedMessages) {
Map<String, List<FormattedMessage>>? actualMessagesMap =
_actualMessages[uri];
if (actualMessagesMap == null) {
print("Error: Allowed-listing of uri '$uri' isn't used. "
"Remove it from the allowed-list.");
errorCount++;
} else {
expectedMessages.forEach((expectedMessage, expectedCount) {
List<FormattedMessage>? actualMessages =
actualMessagesMap[expectedMessage];
if (actualMessages == null) {
print("Error: Allowed-listing of message '$expectedMessage' "
"in uri '$uri' isn't used. Remove it from the allowed-list.");
errorCount++;
} else {
int actualCount = actualMessages.length;
if (actualCount != expectedCount) {
print("Error: Unexpected count of allowed message "
"'$expectedMessage' in uri '$uri'. "
"Expected $expectedCount, actual $actualCount:");
print(
'----------------------------------------------------------');
for (FormattedMessage message in actualMessages) {
onDiagnostic(message);
}
print(
'----------------------------------------------------------');
errorCount++;
}
}
});
actualMessagesMap
.forEach((String message, List<FormattedMessage> actualMessages) {
if (!expectedMessages.containsKey(message)) {
for (FormattedMessage message in actualMessages) {
onDiagnostic(message);
errorCount++;
}
}
});
}
});
_actualMessages.forEach(
(String uri, Map<String, List<FormattedMessage>> actualMessagesMap) {
if (!_expectedJson.containsKey(uri)) {
actualMessagesMap
.forEach((String message, List<FormattedMessage> actualMessages) {
for (FormattedMessage message in actualMessages) {
onDiagnostic(message);
errorCount++;
}
});
}
});
if (errorCount != 0) {
print('$errorCount error(s) found.');
print("""
********************************************************************************
* Unexpected dynamic invocations found by test:
*
* ${relativizeUri(Platform.script)}
*
* Please address the reported errors, or, if the errors are as expected, run
*
* dart ${relativizeUri(Platform.script)} -g
*
* to update the expectation file.
********************************************************************************
""");
exit(-1);
}
if (verbose) {
_actualMessages.forEach(
(String uri, Map<String, List<FormattedMessage>> actualMessagesMap) {
actualMessagesMap
.forEach((String message, List<FormattedMessage> actualMessages) {
for (FormattedMessage message in actualMessages) {
// TODO(johnniwinther): It is unnecessarily complicated to just
// add ' (allowed)' to an existing message!
LocatedMessage locatedMessage = message.locatedMessage;
String newMessageText =
'${locatedMessage.messageObject.problemMessage} (allowed)';
message = locatedMessage.withFormatting(
format(
new LocatedMessage(
locatedMessage.uri,
locatedMessage.charOffset,
locatedMessage.length,
new Message(locatedMessage.messageObject.code,
problemMessage: newMessageText,
correctionMessage:
locatedMessage.messageObject.correctionMessage,
arguments: locatedMessage.messageObject.arguments)),
Severity.warning,
location: new Location(
message.uri!, message.line, message.column),
uriToSource: component.uriToSource),
message.line,
message.column,
Severity.warning,
[]);
onDiagnostic(message);
}
});
});
} else {
int total = 0;
_actualMessages.forEach(
(String uri, Map<String, List<FormattedMessage>> actualMessagesMap) {
int count = 0;
actualMessagesMap
.forEach((String message, List<FormattedMessage> actualMessages) {
count += actualMessages.length;
});
print('${count} error(s) allowed in $uri');
total += count;
});
if (total > 0) {
print('${total} error(s) allowed in total.');
}
}
}
@override
void visitLibrary(Library node) {
if (analyzedUrisFilter != null) {
if (analyzedUrisFilter!(node.importUri)) {
super.visitLibrary(node);
}
} else {
super.visitLibrary(node);
}
}
@override
void visitDynamicGet(DynamicGet node) {
registerError(node, "Dynamic access of '${node.name}'.");
super.visitDynamicGet(node);
}
@override
void visitDynamicSet(DynamicSet node) {
registerError(node, "Dynamic update to '${node.name}'.");
super.visitDynamicSet(node);
}
@override
void visitDynamicInvocation(DynamicInvocation node) {
registerError(node, "Dynamic invocation of '${node.name}'.");
super.visitDynamicInvocation(node);
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
if (checkReturnTypes && node.function.returnType is DynamicType) {
registerError(node, "Dynamic return type");
}
super.visitFunctionDeclaration(node);
}
@override
void visitFunctionExpression(FunctionExpression node) {
if (checkReturnTypes && node.function.returnType is DynamicType) {
registerError(node, "Dynamic return type");
}
super.visitFunctionExpression(node);
}
@override
void visitProcedure(Procedure node) {
if (checkReturnTypes &&
node.function.returnType is DynamicType &&
node.name.text != 'noSuchMethod') {
registerError(node, "Dynamic return type on $node");
}
super.visitProcedure(node);
}
void registerError(TreeNode node, String message) {
Location location = node.location!;
Uri uri = location.file;
String uriString = relativizeUri(uri)!;
Map<String, List<FormattedMessage>> actualMap = _actualMessages.putIfAbsent(
uriString, () => <String, List<FormattedMessage>>{});
if (uri.scheme == 'org-dartlang-sdk') {
location = new Location(Uri.base.resolve(uri.path.substring(1)),
location.line, location.column);
}
LocatedMessage locatedMessage = templateUnspecified
.withArguments(message)
.withLocation(uri, node.fileOffset, noLength);
FormattedMessage diagnosticMessage = locatedMessage.withFormatting(
format(locatedMessage, Severity.warning,
location: location, uriToSource: component.uriToSource),
location.line,
location.column,
Severity.warning,
[]);
actualMap
.putIfAbsent(message, () => <FormattedMessage>[])
.add(diagnosticMessage);
}
}