e9d8109258
Allow enums, extensions, and mixins to use `;` as their body and represent that form explicitly in the AST. The parser now produces `EmptyEnumBody` or `EmptyClassBody` for the empty form. Also replace `LibraryIdentifier` with token-based `DottedName` and store the full token sequence for dotted names. This preserves periods and source offsets directly in the AST, which keeps printing, selection, and directive name handling working with the new shape. See https://github.com/dart-lang/sdk/issues/62819 See https://github.com/dart-lang/language/issues/4645 Google3 presubmit looks green: https://fusion2.corp.google.com/presubmit/884063020/OCL:884063020:BASE:884079610:1773618867493:b2110d76 Change-Id: I2d023cd03b6423da634c3e14742e02a61dc3b403 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/486080 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com>
571 lines
18 KiB
Dart
571 lines
18 KiB
Dart
// 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';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:front_end/src/util/parser_ast.dart';
|
|
import 'package:front_end/src/util/parser_ast_helper.dart';
|
|
|
|
late Uri base;
|
|
|
|
void main(List<String> args) {
|
|
File script = new File.fromUri(Platform.script);
|
|
base = script.parent.uri;
|
|
|
|
testTopLevelStuff();
|
|
testClassStuff();
|
|
testMixinStuff();
|
|
|
|
if (!args.contains("--fast")) {
|
|
canParseTopLevelIshOfAllFrontendFiles();
|
|
}
|
|
}
|
|
|
|
void canParseTopLevelIshOfAllFrontendFiles() {
|
|
Stopwatch stopwatch = new Stopwatch()..start();
|
|
Directory directory = new Directory.fromUri(base.resolve("../../"));
|
|
int processed = 0;
|
|
int errors = 0;
|
|
for (FileSystemEntity entry in directory.listSync(recursive: true)) {
|
|
if (entry is File) {
|
|
if (!entry.path.endsWith(".dart")) continue;
|
|
try {
|
|
processed++;
|
|
Uint8List data = entry.readAsBytesSync();
|
|
CompilationUnitEnd ast = getAST(
|
|
data,
|
|
includeBody: true,
|
|
includeComments: true,
|
|
);
|
|
splitIntoChunks(ast, data);
|
|
for (ParserAstNode child in ast.children!) {
|
|
if (child.isClass()) {
|
|
splitIntoChunks(
|
|
child.asClass().getClassOrMixinOrExtensionBody(),
|
|
data,
|
|
);
|
|
} else if (child.isMixinDeclaration()) {
|
|
MixinDeclarationEnd declaration = child.asMixinDeclaration();
|
|
splitIntoChunks(declaration.getClassOrMixinOrExtensionBody(), data);
|
|
declaration.getNoMixinBody();
|
|
} else if (child.isExtension()) {
|
|
ExtensionDeclarationEnd declaration = child.asExtension();
|
|
splitIntoChunks(declaration.getClassOrMixinOrExtensionBody(), data);
|
|
declaration.getNoExtensionBody();
|
|
} else if (child.isExtensionType()) {
|
|
splitIntoChunks(
|
|
child.asExtensionType().getClassOrMixinOrExtensionBody(),
|
|
data,
|
|
);
|
|
} else if (child.isEnum()) {
|
|
for (MemberEnd member in child.asEnum().getMembers()) {
|
|
processItem(member, data);
|
|
}
|
|
}
|
|
}
|
|
} catch (e, st) {
|
|
print("Failure on $entry:\n$e\n\n$st\n\n--------------\n\n");
|
|
errors++;
|
|
}
|
|
}
|
|
}
|
|
print(
|
|
"Processed $processed files in $directory in ${stopwatch.elapsed}. "
|
|
"Encountered $errors errors.",
|
|
);
|
|
if (errors != 0) {
|
|
throw "Got errors.";
|
|
}
|
|
}
|
|
|
|
void testTopLevelStuff() {
|
|
File file = new File.fromUri(
|
|
base.resolve("parser_ast_test_data/top_level_stuff.txt"),
|
|
);
|
|
Uint8List data = file.readAsBytesSync();
|
|
CompilationUnitEnd ast = getAST(
|
|
data,
|
|
includeBody: true,
|
|
includeComments: true,
|
|
);
|
|
expect(2, ast.getImports().length);
|
|
expect(2, ast.getExports().length);
|
|
|
|
List<String> foundChunks = splitIntoChunks(ast, data);
|
|
expect(23, foundChunks.length);
|
|
expect("library top_level_stuff;", foundChunks[0]);
|
|
expect('import "top_level_stuff_helper.dart";', foundChunks[1]);
|
|
expect('export "top_level_stuff_helper.dart";', foundChunks[2]);
|
|
expect(
|
|
'import "top_level_stuff_helper.dart" show a, b, '
|
|
'c hide d, e, f show foo;',
|
|
foundChunks[3],
|
|
);
|
|
expect(
|
|
'export "top_level_stuff_helper.dart" show a, b, '
|
|
'c hide d, e, f show foo;',
|
|
foundChunks[4],
|
|
);
|
|
expect("part 'top_level_stuff_helper.dart';", foundChunks[5]);
|
|
expect('@metadataOneOnThisOne("bla")', foundChunks[6]);
|
|
expect("@metadataTwoOnThisOne", foundChunks[7]);
|
|
expect('@metadataThree.OnThisOne<int>("hello")', foundChunks[8]);
|
|
expect("""void toplevelMethod() {
|
|
// no content
|
|
}""", foundChunks[9]);
|
|
expect("""List<E> anotherTopLevelMethod<E>() {
|
|
return null;
|
|
}""", foundChunks[10]);
|
|
expect("enum FooEnum { A, B, Bla }", foundChunks[11]);
|
|
expect("""class FooClass {
|
|
// no content.
|
|
}""", foundChunks[12]);
|
|
expect("""mixin FooMixin {
|
|
// no content.
|
|
}""", foundChunks[13]);
|
|
expect("""class A<T> {
|
|
// no content.
|
|
}""", foundChunks[14]);
|
|
expect("typedef B = Function();", foundChunks[15]);
|
|
expect("""mixin C<T> on A<T> {
|
|
// no content.
|
|
}""", foundChunks[16]);
|
|
expect("""extension D<T> on A<T> {
|
|
// no content.
|
|
}""", foundChunks[17]);
|
|
expect("class E = A with FooClass;", foundChunks[18]);
|
|
expect("int field1;", foundChunks[19]);
|
|
expect("int field2, field3;", foundChunks[20]);
|
|
expect("int field4 = 42;", foundChunks[21]);
|
|
expect("@AnnotationAtEOF", foundChunks[22]);
|
|
|
|
file = new File.fromUri(
|
|
base.resolve("parser_ast_test_data/top_level_stuff_helper.txt"),
|
|
);
|
|
data = file.readAsBytesSync();
|
|
ast = getAST(data, includeBody: true, includeComments: true);
|
|
foundChunks = splitIntoChunks(ast, data);
|
|
expect(1, foundChunks.length);
|
|
expect("part of 'top_level_stuff.txt';", foundChunks[0]);
|
|
|
|
file = new File.fromUri(
|
|
base.resolve("parser_ast_test_data/script_handle.txt"),
|
|
);
|
|
data = file.readAsBytesSync();
|
|
ast = getAST(data, includeBody: true, includeComments: true);
|
|
foundChunks = splitIntoChunks(ast, data);
|
|
expect(1, foundChunks.length);
|
|
expect("#!/usr/bin/env dart -c", foundChunks[0]);
|
|
}
|
|
|
|
void testClassStuff() {
|
|
File file = new File.fromUri(base.resolve("parser_ast_test_data/class.txt"));
|
|
Uint8List data = file.readAsBytesSync();
|
|
CompilationUnitEnd ast = getAST(
|
|
data,
|
|
includeBody: true,
|
|
includeComments: true,
|
|
);
|
|
List<TopLevelDeclarationEnd> classes = ast.getClasses();
|
|
expect(2, classes.length);
|
|
|
|
TopLevelDeclarationEnd decl = classes[0];
|
|
ClassDeclarationEnd cls = decl.asClass();
|
|
expect("Foo", decl.getIdentifier().token.lexeme);
|
|
ClassExtendsHandle extendsDecl = cls.getClassExtends();
|
|
expect("extends", extendsDecl.extendsKeyword?.lexeme);
|
|
ImplementsHandle implementsDecl = cls.getClassImplements();
|
|
expect("implements", implementsDecl.implementsKeyword?.lexeme);
|
|
ClassWithClauseHandle? withClauseDecl = cls.getClassWithClause();
|
|
expect(null, withClauseDecl);
|
|
List<MemberEnd> members = cls.getClassOrMixinOrExtensionBody().getMembers();
|
|
expect(5, members.length);
|
|
expect(members[0].isConstructor(), true);
|
|
expect(members[1].isFactory(), true);
|
|
expect(members[2].isMethod(), true);
|
|
expect(members[3].isMethod(), true);
|
|
expect(members[4].isFields(), true);
|
|
|
|
List<String> chunks = splitIntoChunks(
|
|
cls.getClassOrMixinOrExtensionBody(),
|
|
data,
|
|
);
|
|
expect(5, chunks.length);
|
|
expect("""Foo() {
|
|
// Constructor
|
|
}""", chunks[0]);
|
|
expect("factory Foo.factory() => Foo();", chunks[1]);
|
|
expect("""void method() {
|
|
// instance method.
|
|
}""", chunks[2]);
|
|
expect("""static void staticMethod() {
|
|
// static method.
|
|
}""", chunks[3]);
|
|
expect("int field1, field2 = 42;", chunks[4]);
|
|
|
|
chunks = processItem(
|
|
members[0].getConstructor().getBlockFunctionBody()!,
|
|
data,
|
|
);
|
|
expect(1, chunks.length);
|
|
expect("""{
|
|
// Constructor
|
|
}""", chunks[0]);
|
|
chunks = processItem(members[2].getMethod().getBlockFunctionBody()!, data);
|
|
expect(1, chunks.length);
|
|
expect("""{
|
|
// instance method.
|
|
}""", chunks[0]);
|
|
chunks = processItem(members[3].getMethod().getBlockFunctionBody()!, data);
|
|
expect(1, chunks.length);
|
|
expect("""{
|
|
// static method.
|
|
}""", chunks[0]);
|
|
|
|
// TODO: Move (something like) this into the check-all-files-thing.
|
|
for (MemberEnd member in cls.getClassOrMixinOrExtensionBody().getMembers()) {
|
|
if (member.isConstructor()) continue;
|
|
if (member.isFactory()) continue;
|
|
if (member.isFields()) continue;
|
|
if (member.isMethod()) continue;
|
|
throw "$member --- ${member.children}";
|
|
}
|
|
|
|
decl = classes[1];
|
|
cls = decl.asClass();
|
|
expect("Foo2", decl.getIdentifier().token.lexeme);
|
|
extendsDecl = cls.getClassExtends();
|
|
expect(null, extendsDecl.extendsKeyword?.lexeme);
|
|
implementsDecl = cls.getClassImplements();
|
|
expect(null, implementsDecl.implementsKeyword?.lexeme);
|
|
withClauseDecl = cls.getClassWithClause();
|
|
expect("with", withClauseDecl!.withKeyword.lexeme);
|
|
members = cls.getClassOrMixinOrExtensionBody().getMembers();
|
|
expect(0, members.length);
|
|
}
|
|
|
|
void testMixinStuff() {
|
|
File file = new File.fromUri(base.resolve("parser_ast_test_data/mixin.txt"));
|
|
Uint8List data = file.readAsBytesSync();
|
|
CompilationUnitEnd ast = getAST(
|
|
data,
|
|
includeBody: true,
|
|
includeComments: true,
|
|
);
|
|
List<TopLevelDeclarationEnd> mixins = ast.getMixinDeclarations();
|
|
expect(mixins.length, 1);
|
|
|
|
TopLevelDeclarationEnd decl = mixins[0];
|
|
MixinDeclarationEnd mxn = decl.asMixinDeclaration();
|
|
expect("B", decl.getIdentifier().token.lexeme);
|
|
|
|
ClassOrMixinOrExtensionBodyEnd? body = mxn.getClassOrMixinOrExtensionBody();
|
|
expect(false, body == null);
|
|
List<MemberEnd> members = body.getMembers();
|
|
expect(4, members.length);
|
|
expect(members[0].isFields(), true);
|
|
expect(members[1].isMethod(), true);
|
|
expect(members[2].isFactory(), true);
|
|
expect(members[3].isConstructor(), true);
|
|
|
|
List<String> chunks = splitIntoChunks(body, data);
|
|
expect(4, chunks.length);
|
|
expect("static int staticField = 0;", chunks[0]);
|
|
expect("""void foo() {
|
|
// empty
|
|
}""", chunks[1]);
|
|
expect("""factory B() {
|
|
// empty
|
|
}""", chunks[2]);
|
|
expect("""B.foo() {
|
|
// empty
|
|
}""", chunks[3]);
|
|
}
|
|
|
|
void expect<E>(E expect, E actual) {
|
|
if (expect != actual) throw "Expected '$expect' but got '$actual'";
|
|
}
|
|
|
|
List<String> splitIntoChunks(ParserAstNode? ast, List<int> data) {
|
|
List<String> foundChunks = [];
|
|
List<ParserAstNode>? children = ast?.children;
|
|
if (children != null) {
|
|
for (ParserAstNode child in children) {
|
|
foundChunks.addAll(processItem(child, data));
|
|
}
|
|
}
|
|
return foundChunks;
|
|
}
|
|
|
|
List<String> processItem(ParserAstNode item, List<int> data) {
|
|
if (item.isClass()) {
|
|
ClassDeclarationEnd cls = item.asClass();
|
|
// Check that we can get the identifier without throwing.
|
|
cls.getClassIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
cls.beginToken.offset,
|
|
cls.endToken.offset + cls.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isMetadata()) {
|
|
MetadataStarEnd metadataStar = item.asMetadata();
|
|
List<MetadataEnd> entries = metadataStar.getMetadataEntries();
|
|
if (entries.isNotEmpty) {
|
|
List<String> chunks = [];
|
|
for (MetadataEnd metadata in entries) {
|
|
// Check that we can get the identifiers without throwing.
|
|
metadata.getIdentifiers();
|
|
chunks.add(
|
|
getCutContent(
|
|
data,
|
|
metadata.beginToken.offset,
|
|
metadata.endToken.charEnd,
|
|
),
|
|
);
|
|
}
|
|
return chunks;
|
|
}
|
|
return const [];
|
|
} else if (item.isImport()) {
|
|
ImportEnd import = item.asImport();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
import.importKeyword.offset,
|
|
import.semicolon!.offset + import.semicolon!.length,
|
|
),
|
|
];
|
|
} else if (item.isExport()) {
|
|
ExportEnd export = item.asExport();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
export.exportKeyword.offset,
|
|
export.semicolon.offset + export.semicolon.length,
|
|
),
|
|
];
|
|
} else if (item.isLibraryName()) {
|
|
LibraryNameEnd name = item.asLibraryName();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
name.libraryKeyword.offset,
|
|
name.semicolon.offset + name.semicolon.length,
|
|
),
|
|
];
|
|
} else if (item.isPart()) {
|
|
PartEnd part = item.asPart();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
part.partKeyword.offset,
|
|
part.semicolon.offset + part.semicolon.length,
|
|
),
|
|
];
|
|
} else if (item.isPartOf()) {
|
|
PartOfEnd partOf = item.asPartOf();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
partOf.partKeyword.offset,
|
|
partOf.semicolon.offset + partOf.semicolon.length,
|
|
),
|
|
];
|
|
} else if (item.isTopLevelMethod()) {
|
|
TopLevelMethodEnd method = item.asTopLevelMethod();
|
|
// Check that we can get the identifier without throwing.
|
|
method.getNameIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
method.beginToken.offset,
|
|
method.endToken.offset + method.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isTopLevelFields()) {
|
|
TopLevelFieldsEnd fields = item.asTopLevelFields();
|
|
// Check that we can get the identifiers without throwing.
|
|
fields.getFieldIdentifiers();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
fields.beginToken.offset,
|
|
fields.endToken.offset + fields.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isEnum()) {
|
|
EnumDeclarationEnd declaration = item.asEnum();
|
|
// Check that we can get the identifier without throwing.
|
|
declaration.getEnumIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
declaration.enumKeyword.offset,
|
|
declaration.leftBrace.endGroup!.offset +
|
|
declaration.leftBrace.endGroup!.length,
|
|
),
|
|
];
|
|
} else if (item.isMixinDeclaration()) {
|
|
MixinDeclarationEnd mixinDecl = item.asMixinDeclaration();
|
|
// Check that we can get the identifier without throwing.
|
|
mixinDecl.getMixinIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
mixinDecl.beginToken.offset,
|
|
mixinDecl.endToken.offset + mixinDecl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isNamedMixinDeclaration()) {
|
|
NamedMixinApplicationEnd namedMixinDecl = item.asNamedMixinDeclaration();
|
|
// Check that we can get the identifier without throwing.
|
|
namedMixinDecl.getMixinIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
namedMixinDecl.begin.offset,
|
|
namedMixinDecl.endToken.offset + namedMixinDecl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isTypedef()) {
|
|
TypedefEnd typedefDecl = item.asTypedef();
|
|
// Check that we can get the identifier without throwing.
|
|
typedefDecl.getNameIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
typedefDecl.typedefKeyword.offset,
|
|
typedefDecl.endToken.offset + typedefDecl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isExtension()) {
|
|
ExtensionDeclarationEnd extensionDecl = item.asExtension();
|
|
// Check that we can get the identifier without throwing.
|
|
extensionDecl.getExtensionName();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
extensionDecl.extensionKeyword.offset,
|
|
extensionDecl.endToken.offset + extensionDecl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isExtensionType()) {
|
|
ExtensionTypeDeclarationEnd extensionTypeDecl = item.asExtensionType();
|
|
// Check that we can get the identifier without throwing.
|
|
extensionTypeDecl.getExtensionTypeName();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
extensionTypeDecl.extensionKeyword.offset,
|
|
extensionTypeDecl.endToken.offset + extensionTypeDecl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isScript()) {
|
|
ScriptHandle script = item.asScript();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
script.token.offset,
|
|
script.token.offset + script.token.length,
|
|
),
|
|
];
|
|
} else if (item is MemberEnd) {
|
|
if (item.isConstructor()) {
|
|
ConstructorEnd decl = item.getConstructor();
|
|
// Check that we can get the identifiers without throwing.
|
|
decl.getIdentifiers();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isFactory()) {
|
|
FactoryEnd decl = item.getFactory();
|
|
// Check that we can get the identifiers without throwing.
|
|
decl.getIdentifiers();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isMethod()) {
|
|
MethodEnd decl = item.getMethod();
|
|
// Check that we can get the identifier without throwing.
|
|
decl.getNameIdentifier();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isFields()) {
|
|
FieldsEnd decl = item.getFields();
|
|
// Check that we can get the identifiers without throwing.
|
|
decl.getFieldIdentifiers();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else if (item.isPrimaryConstructorBody()) {
|
|
PrimaryConstructorBodyEnd decl = item.getPrimaryConstructorBody();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else {
|
|
if (item.type == ParserAstType.BEGIN) return const [];
|
|
if (item.type == ParserAstType.HANDLE) return const [];
|
|
if (item.isClassRecoverableError()) return const [];
|
|
if (item.isExperimentNotEnabled()) return const [];
|
|
if (item.isRecoverableError()) return const [];
|
|
if (item.isRecoverImport()) return const [];
|
|
throw "Unknown member: $item --- ${item.children}";
|
|
}
|
|
} else if (item.isFunctionBody()) {
|
|
BlockFunctionBodyEnd decl = item.asFunctionBody();
|
|
return [
|
|
getCutContent(
|
|
data,
|
|
decl.beginToken.offset,
|
|
decl.endToken.offset + decl.endToken.length,
|
|
),
|
|
];
|
|
} else {
|
|
if (item.type == ParserAstType.BEGIN) return const [];
|
|
if (item.type == ParserAstType.HANDLE) return const [];
|
|
if (item.isInvalidTopLevelDeclaration()) return const [];
|
|
if (item.isRecoverableError()) return const [];
|
|
if (item.isRecoverImport()) return const [];
|
|
|
|
throw "Unknown: $item --- ${item.children}";
|
|
}
|
|
}
|
|
|
|
List<int>? _contentCache;
|
|
String? _contentCacheString;
|
|
String getCutContent(List<int> content, int from, int to) {
|
|
if (identical(content, _contentCache)) {
|
|
// cache up to date.
|
|
} else {
|
|
_contentCache = content;
|
|
_contentCacheString = utf8.decode(content);
|
|
}
|
|
return _contentCacheString!.substring(from, to);
|
|
}
|