blob: 16bc592683777a71a19cc482257ef5ca714fa79c [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 Directory, File, FileSystemEntity;
import 'dart:typed_data' show Uint8List;
import 'package:_fe_analyzer_shared/src/parser/parser.dart'
show FormalParameterKind, MemberKind, Parser;
import 'package:_fe_analyzer_shared/src/parser/listener.dart' show Listener;
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/fasta/command_line_reporting.dart'
as command_line_reporting;
import 'package:front_end/src/fasta/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, runMe;
import 'testing_utils.dart' show checkEnvironment, getGitFiles;
void main([List<String> arguments = const []]) =>
runMe(arguments, createContext, 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 special handling of negative tests.
@override
Result processTestResult(
TestDescription description, Result result, bool last) {
return result;
}
@override
Stream<LintTestDescription> list(Chain suite) async* {
late Set<Uri> gitFiles;
if (onlyInGit) {
gitFiles = await getGitFiles(suite.uri);
}
Directory testRoot = new Directory.fromUri(suite.uri);
if (await testRoot.exists()) {
Stream<FileSystemEntity> files =
testRoot.list(recursive: true, followLinks: false);
await for (FileSystemEntity entity in files) {
if (entity is! File) continue;
String path = entity.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
if (suite.pattern.any((RegExp r) => path.contains(r))) {
if (onlyInGit && !gitFiles.contains(entity.uri)) continue;
Uri root = suite.uri;
String baseName = "${entity.uri}".substring("$root".length);
baseName = baseName.substring(0, baseName.length - ".dart".length);
LintTestCache cache = new LintTestCache();
yield new LintTestDescription(
"$baseName/ExplicitType",
entity.uri,
cache,
new ExplicitTypeLintListener(),
);
yield new LintTestDescription(
"$baseName/ImportsTwice",
entity.uri,
cache,
new ImportsTwiceLintListener(),
);
Uri apiUnstableUri =
Uri.base.resolve("pkg/front_end/lib/src/api_unstable/");
if (!entity.uri.toString().startsWith(apiUnstableUri.toString())) {
yield new LintTestDescription(
"$baseName/Exports",
entity.uri,
cache,
new ExportsLintListener(),
);
}
}
}
} else {
throw "${suite.uri} isn't a directory";
}
}
}
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, includeComments: true);
description.cache.firstToken = scanner.tokenize();
description.cache.lineStarts = scanner.lineStarts;
Uri dotPackages =
description.uri.resolve(".dart_tool/package_config.json");
while (true) {
if (new File.fromUri(dotPackages).existsSync()) {
break;
}
// Stupid bailout.
if (dotPackages.pathSegments.length < Uri.base.pathSegments.length) {
break;
}
dotPackages =
dotPackages.resolve("../../.dart_tool/package_config.json");
}
File dotPackagesFile = new File.fromUri(dotPackages);
if (dotPackagesFile.existsSync()) {
description.cache.packages = await loadPackageConfigUri(dotPackages);
}
}
if (description.cache.firstToken == null) {
return crash(description, StackTrace.current);
}
Parser parser = new Parser(description.listener,
useImplicitCreationExpression: useImplicitCreationExpressionInCfe);
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? 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? 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? 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")) {
if (description.cache.packages != null) {
resolved = description.cache.packages!.resolve(resolved)!;
}
}
onProblem(exportUriToken.offset, exportUriToken.lexeme.length,
"Exports disallowed internally.");
}
}