blob: 3a2e35887c07f44df0be1cb26e9c4ffdbc351fc4 [file] [log] [blame]
// Copyright (c) 2019, 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:io' show File;
import 'dart:typed_data' show Uint8List;
import 'package:_fe_analyzer_shared/src/parser/listener.dart' show Listener;
import 'package:_fe_analyzer_shared/src/parser/parser.dart'
show FormalParameterKind, MemberKind, Parser;
import 'package:_fe_analyzer_shared/src/scanner/abstract_scanner.dart'
show ScannerConfiguration;
import 'package:_fe_analyzer_shared/src/scanner/token.dart' show Token;
import 'package:_fe_analyzer_shared/src/scanner/utf8_bytes_scanner.dart'
show Utf8BytesScanner;
import 'package:front_end/src/base/command_line_reporting.dart'
as command_line_reporting;
import 'package:front_end/src/source/diet_parser.dart'
show useImplicitCreationExpressionInCfe;
import 'package:kernel/kernel.dart';
import 'package:package_config/package_config.dart';
import 'package:testing/testing.dart'
show Chain, ChainContext, Result, Step, TestDescription;
import 'fasta/suite_utils.dart';
import 'testing_utils.dart' show checkEnvironment, filterList;
void main([List<String> arguments = const []]) => internalMain(createContext,
arguments: arguments,
displayName: "lint suite",
configurationPath: "../testing.json");
Future<Context> createContext(
Chain suite, Map<String, String> environment) async {
const Set<String> knownEnvironmentKeys = {"onlyInGit"};
checkEnvironment(environment, knownEnvironmentKeys);
bool onlyInGit = environment["onlyInGit"] != "false";
return new Context(onlyInGit: onlyInGit);
}
class LintTestDescription extends TestDescription {
@override
final String shortName;
@override
final Uri uri;
final LintTestCache cache;
final LintListener listener;
LintTestDescription(this.shortName, this.uri, this.cache, this.listener) {
this.listener.description = this;
this.listener.uri = uri;
}
String getErrorMessage(int offset, int squigglyLength, String message) {
cache.source ??= new Source(cache.lineStarts, cache.rawBytes!, uri, uri);
Location location = cache.source!.getLocation(uri, offset);
return command_line_reporting.formatErrorMessage(
cache.source!.getTextLine(location.line),
location,
squigglyLength,
uri.toString(),
message);
}
}
class LintTestCache {
List<int>? rawBytes;
late List<int> lineStarts;
Source? source;
Token? firstToken;
PackageConfig? packages;
}
class Context extends ChainContext {
final bool onlyInGit;
Context({required this.onlyInGit});
@override
final List<Step> steps = const <Step>[
const LintStep(),
];
@override
Future<List<LintTestDescription>> list(Chain suite) async {
String rootString = "${suite.root}";
Uri apiUnstableUri =
Uri.base.resolve("pkg/front_end/lib/src/api_unstable/");
String apiUnstableString = apiUnstableUri.toString();
List<LintTestDescription> result = [];
for (TestDescription description
in await filterList(suite, onlyInGit, await super.list(suite))) {
String baseName = "${description.uri}".substring(rootString.length);
baseName = baseName.substring(0, baseName.length - ".dart".length);
LintTestCache cache = new LintTestCache();
result.add(new LintTestDescription(
"$baseName/ExplicitType",
description.uri,
cache,
new ExplicitTypeLintListener(),
));
result.add(new LintTestDescription(
"$baseName/ImportsTwice",
description.uri,
cache,
new ImportsTwiceLintListener(),
));
if (!description.uri.toString().startsWith(apiUnstableString)) {
result.add(new LintTestDescription(
"$baseName/Exports",
description.uri,
cache,
new ExportsLintListener(),
));
}
}
return result;
}
}
class LintStep extends Step<LintTestDescription, LintTestDescription, Context> {
const LintStep();
@override
String get name => "lint";
@override
Future<Result<LintTestDescription>> run(
LintTestDescription description, Context context) async {
if (description.cache.rawBytes == null) {
File f = new File.fromUri(description.uri);
description.cache.rawBytes = f.readAsBytesSync();
Uint8List bytes = new Uint8List(description.cache.rawBytes!.length + 1);
bytes.setRange(
0, description.cache.rawBytes!.length, description.cache.rawBytes!);
Utf8BytesScanner scanner = new Utf8BytesScanner(
bytes,
configuration: const ScannerConfiguration(
enableExtensionMethods: true,
enableNonNullable: true,
enableTripleShift: true),
includeComments: true,
languageVersionChanged: (scanner, languageVersion) {
// Nothing - but don't overwrite the previous settings.
},
);
description.cache.firstToken = scanner.tokenize();
description.cache.lineStarts = scanner.lineStarts;
Uri packageConfig =
description.uri.resolve(".dart_tool/package_config.json");
while (true) {
if (new File.fromUri(packageConfig).existsSync()) {
break;
}
// Stupid bailout.
if (packageConfig.pathSegments.length < Uri.base.pathSegments.length) {
break;
}
packageConfig =
packageConfig.resolve("../../.dart_tool/package_config.json");
}
File packageConfigUri = new File.fromUri(packageConfig);
if (packageConfigUri.existsSync()) {
description.cache.packages = await loadPackageConfigUri(packageConfig);
}
}
if (description.cache.firstToken == null) {
return crash(description, StackTrace.current);
}
Parser parser = new Parser(description.listener,
useImplicitCreationExpression: useImplicitCreationExpressionInCfe,
allowPatterns: true);
parser.parseUnit(description.cache.firstToken!);
if (description.listener.problems.isEmpty) {
return pass(description);
}
return fail(description, description.listener.problems.join("\n\n"));
}
}
class LintListener extends Listener {
List<String> problems = <String>[];
late final LintTestDescription description;
@override
late final Uri uri;
void onProblem(int offset, int squigglyLength, String message) {
problems.add(description.getErrorMessage(offset, squigglyLength, message));
}
}
class ExplicitTypeLintListener extends LintListener {
List<LatestType> _latestTypes = <LatestType>[];
@override
void beginVariablesDeclaration(
Token token, Token? lateToken, Token? varFinalOrConst) {
if (!_latestTypes.last.type) {
onProblem(
varFinalOrConst!.offset, varFinalOrConst.length, "No explicit type.");
}
}
@override
void handleType(Token beginToken, Token? questionMark) {
_latestTypes.add(new LatestType(beginToken, true));
}
@override
void handleNoType(Token lastConsumed) {
_latestTypes.add(new LatestType(lastConsumed, false));
}
@override
void endFunctionType(Token functionToken, Token? questionMark) {
_latestTypes.add(new LatestType(functionToken, true));
}
@override
void endTopLevelFields(
Token? augmentToken,
Token? externalToken,
Token? staticToken,
Token? covariantToken,
Token? lateToken,
Token? varFinalOrConst,
int count,
Token beginToken,
Token endToken) {
if (!_latestTypes.last.type) {
onProblem(beginToken.offset, beginToken.length, "No explicit type.");
}
_latestTypes.removeLast();
}
@override
void endClassFields(
Token? abstractToken,
Token? augmentToken,
Token? externalToken,
Token? staticToken,
Token? covariantToken,
Token? lateToken,
Token? varFinalOrConst,
int count,
Token beginToken,
Token endToken) {
if (!_latestTypes.last.type) {
onProblem(
varFinalOrConst!.offset, varFinalOrConst.length, "No explicit type.");
}
_latestTypes.removeLast();
}
@override
void endFormalParameter(
Token? thisKeyword,
Token? superKeyword,
Token? periodAfterThisOrSuper,
Token nameToken,
Token? initializerStart,
Token? initializerEnd,
FormalParameterKind kind,
MemberKind memberKind) {
_latestTypes.removeLast();
}
}
class LatestType {
final Token token;
bool type;
LatestType(this.token, this.type);
}
class ImportsTwiceLintListener extends LintListener {
Map<Uri, Set<String?>> seenImports = {};
Token? seenAsKeyword;
@override
void handleImportPrefix(Token? deferredKeyword, Token? asKeyword) {
seenAsKeyword = asKeyword;
}
@override
void endImport(Token importKeyword, Token? augmentToken, Token? semicolon) {
Token importUriToken = importKeyword.next!;
String importUri = importUriToken.lexeme;
if (importUri.startsWith("r")) {
importUri = importUri.substring(2, importUri.length - 1);
} else {
importUri = importUri.substring(1, importUri.length - 1);
}
Uri resolved = uri.resolve(importUri);
if (resolved.isScheme("package")) {
if (description.cache.packages != null) {
resolved = description.cache.packages!.resolve(resolved)!;
}
}
String? asName = seenAsKeyword?.lexeme;
Set<String?> asNames = seenImports[resolved] ??= {};
if (!asNames.add(asName)) {
if (asName != null) {
onProblem(importUriToken.offset, importUriToken.lexeme.length,
"Uri '$resolved' already imported once as '${asName}'.");
} else {
onProblem(importUriToken.offset, importUriToken.lexeme.length,
"Uri '$resolved' already imported once.");
}
}
}
}
class ExportsLintListener extends LintListener {
@override
void endExport(Token exportKeyword, Token semicolon) {
Token exportUriToken = exportKeyword.next!;
String exportUri = exportUriToken.lexeme;
if (exportUri.startsWith("r")) {
exportUri = exportUri.substring(2, exportUri.length - 1);
} else {
exportUri = exportUri.substring(1, exportUri.length - 1);
}
Uri resolved = uri.resolve(exportUri);
if (resolved.isScheme("package") && resolved.path.startsWith('kernel/')) {
// Exporting from `package:kernel` is allowed.
return;
}
// Allowlist specific exports.
if (resolved.isScheme('package') &&
['_fe_analyzer_shared/src/macros/uri.dart'].contains(resolved.path)) {
return;
}
if (resolved.isScheme("package")) {
if (description.cache.packages != null) {
resolved = description.cache.packages!.resolve(resolved)!;
}
}
onProblem(exportUriToken.offset, exportUriToken.lexeme.length,
"Exports disallowed internally.");
}
}