blob: d80887461fa8e82bca82e2fea01c2dbee1d59004 [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:typed_data' show Uint8List;
import 'dart:io' show File;
import 'package:_fe_analyzer_shared/src/parser/class_member_parser.dart'
show ClassMemberParser;
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
show ErrorToken, LanguageVersionToken, Scanner;
import 'package:_fe_analyzer_shared/src/scanner/utf8_bytes_scanner.dart'
show Utf8BytesScanner;
import '../../fasta/source/directive_listener.dart' show DirectiveListener;
import 'package:_fe_analyzer_shared/src/scanner/token.dart' show Token;
abstract class _Chunk implements Comparable<_Chunk> {
final int originalPosition;
List<_MetadataChunk> metadata;
_Chunk(this.originalPosition);
void printOn(StringBuffer sb, String indent);
void printMetadata(StringBuffer sb, String indent) {
if (metadata != null) {
for (_MetadataChunk m in metadata) {
m.printMetadataOn(sb, indent);
}
}
}
void internalMergeAndSort();
@override
int compareTo(_Chunk other) {
// Generally we compare according to the original position.
if (originalPosition < other.originalPosition) return -1;
return 1;
}
}
class _LanguageVersionChunk extends _Chunk {
final int major;
final int minor;
_LanguageVersionChunk(int originalPosition, this.major, this.minor)
: super(originalPosition);
@override
void printOn(StringBuffer sb, String indent) {
if (sb.isNotEmpty) {
sb.write("\n\n");
}
printMetadata(sb, indent);
sb.write("// @dart = ${major}.${minor}");
}
@override
void internalMergeAndSort() {
// Cannot be sorted.
}
}
abstract class _TokenChunk extends _Chunk {
final Token startToken;
final Token endToken;
_TokenChunk(int originalPosition, this.startToken, this.endToken)
: super(originalPosition);
void printOn(StringBuffer sb, String indent) {
int endOfLast = startToken.end;
if (sb.isNotEmpty) {
sb.write("\n");
if (indent.isEmpty && this is! _SingleImportExportChunk) {
// Hack to imitate v1.
sb.write("\n");
}
}
printMetadata(sb, indent);
sb.write(indent);
Token token = startToken;
Token afterEnd = endToken.next;
while (token != afterEnd) {
if (token.offset > endOfLast) {
sb.write(" ");
}
sb.write(token.lexeme);
endOfLast = token.end;
token = token.next;
}
}
@override
void internalMergeAndSort() {
// Generally cannot be sorted.
}
}
abstract class _SortableChunk extends _TokenChunk {
_SortableChunk(int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
@override
int compareTo(_Chunk o) {
if (o is! _SortableChunk) return super.compareTo(o);
_SortableChunk other = o;
// Compare lexemes from startToken and at most the next 10 tokens.
// For valid code this should be more than enough. Note that this won't
// sort as a text-sort would as for instance "C<Foo>" and "C2<Foo>" will
// say "C" < "C2" where a text-sort would say "C<" > "C2". This doesn't
// really matter as long as the sorting is consistent (i.e. the textual
// outline always sorts like this).
Token thisToken = startToken;
Token otherToken = other.startToken;
int steps = 0;
while (thisToken.lexeme == otherToken.lexeme) {
if (steps++ > 10) break;
thisToken = thisToken.next;
otherToken = otherToken.next;
}
if (thisToken.lexeme == otherToken.lexeme) return super.compareTo(o);
return thisToken.lexeme.compareTo(otherToken.lexeme);
}
}
class _ImportExportChunk extends _Chunk {
final List<_SingleImportExportChunk> content =
new List<_SingleImportExportChunk>();
_ImportExportChunk(int originalPosition) : super(originalPosition);
@override
void printOn(StringBuffer sb, String indent) {
if (sb.isNotEmpty) {
sb.write("\n");
}
printMetadata(sb, indent);
for (_SingleImportExportChunk chunk in content) {
chunk.printOn(sb, indent);
}
}
@override
void internalMergeAndSort() {
content.sort();
}
}
class _SingleImportExportChunk extends _SortableChunk {
_SingleImportExportChunk(
int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
}
class _KnownUnsortableChunk extends _TokenChunk {
_KnownUnsortableChunk(int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
}
class _ClassChunk extends _SortableChunk {
List<_Chunk> content = new List<_Chunk>();
Token headerEnd;
Token footerStart;
_ClassChunk(int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
void printOn(StringBuffer sb, String indent) {
int endOfLast = startToken.end;
if (sb.isNotEmpty) {
sb.write("\n\n");
sb.write(indent);
}
printMetadata(sb, indent);
// Header.
Token token = startToken;
Token afterEnd = headerEnd.next;
while (token != afterEnd) {
if (token.offset > endOfLast) {
sb.write(" ");
}
sb.write(token.lexeme);
endOfLast = token.end;
token = token.next;
}
// Content.
for (_Chunk chunk in content) {
chunk.printOn(sb, " $indent");
}
// Footer.
if (footerStart != null) {
if (content.isNotEmpty) {
sb.write("\n");
sb.write(indent);
}
endOfLast = footerStart.end;
token = footerStart;
afterEnd = endToken.next;
while (token != afterEnd) {
if (token.offset > endOfLast) {
sb.write(" ");
}
sb.write(token.lexeme);
endOfLast = token.end;
token = token.next;
}
}
}
@override
void internalMergeAndSort() {
content = _mergeAndSort(content);
}
}
class _ProcedureEtcChunk extends _SortableChunk {
final Set<int> nonClassEndOffsets;
_ProcedureEtcChunk(int originalPosition, Token startToken, Token endToken,
this.nonClassEndOffsets)
: super(originalPosition, startToken, endToken);
void printOn(StringBuffer sb, String indent) {
int endOfLast = startToken.end;
if (sb.isNotEmpty) {
sb.write("\n");
if (indent.isEmpty) {
// Hack to imitate v1.
sb.write("\n");
}
}
printMetadata(sb, indent);
sb.write(indent);
Token token = startToken;
Token afterEnd = endToken.next;
bool nextTokenIsEndGroup = false;
while (token != afterEnd) {
if (token.offset > endOfLast && !nextTokenIsEndGroup) {
sb.write(" ");
}
sb.write(token.lexeme);
endOfLast = token.end;
if (token.endGroup != null &&
nonClassEndOffsets.contains(token.endGroup.offset)) {
token = token.endGroup;
nextTokenIsEndGroup = true;
} else {
token = token.next;
nextTokenIsEndGroup = false;
}
}
}
}
class _MetadataChunk extends _TokenChunk {
_MetadataChunk(int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
void printMetadataOn(StringBuffer sb, String indent) {
int endOfLast = startToken.end;
sb.write(indent);
Token token = startToken;
Token afterEnd = endToken.next;
while (token != afterEnd) {
if (token.offset > endOfLast) {
sb.write(" ");
}
sb.write(token.lexeme);
endOfLast = token.end;
token = token.next;
}
sb.write("\n");
}
}
class _UnknownChunk extends _TokenChunk {
_UnknownChunk(int originalPosition, Token startToken, Token endToken)
: super(originalPosition, startToken, endToken);
}
class _UnknownTokenBuilder {
Token start;
Token interimEnd;
}
class BoxedInt {
int value;
BoxedInt(this.value);
}
// TODO(jensj): Better support for show/hide on imports/exports.
String textualOutline(List<int> rawBytes,
{bool throwOnUnexpected: false,
bool performModelling: false,
bool addMarkerForUnknownForTest: false}) {
// TODO(jensj): We need to specify the scanner settings to match that of the
// compiler!
Uint8List bytes = new Uint8List(rawBytes.length + 1);
bytes.setRange(0, rawBytes.length, rawBytes);
List<_Chunk> parsedChunks = new List<_Chunk>();
BoxedInt originalPosition = new BoxedInt(0);
Utf8BytesScanner scanner = new Utf8BytesScanner(bytes, includeComments: false,
languageVersionChanged:
(Scanner scanner, LanguageVersionToken languageVersion) {
parsedChunks.add(new _LanguageVersionChunk(originalPosition.value++,
languageVersion.major, languageVersion.minor));
});
Token firstToken = scanner.tokenize();
if (firstToken == null) {
if (throwOnUnexpected) throw "firstToken is null";
return null;
}
TextualOutlineListener listener = new TextualOutlineListener();
ClassMemberParser classMemberParser = new ClassMemberParser(listener);
classMemberParser.parseUnit(firstToken);
Token nextToken = firstToken;
_UnknownTokenBuilder currentUnknown = new _UnknownTokenBuilder();
while (nextToken != null) {
if (nextToken is ErrorToken) {
return null;
}
if (nextToken.isEof) break;
nextToken = _textualizeTokens(
listener, nextToken, currentUnknown, parsedChunks, originalPosition);
}
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
if (nextToken == null) return null;
if (performModelling) {
parsedChunks = _mergeAndSort(parsedChunks);
}
StringBuffer sb = new StringBuffer();
for (_Chunk chunk in parsedChunks) {
chunk.printOn(sb, "");
}
return sb.toString();
}
List<_Chunk> _mergeAndSort(List<_Chunk> chunks) {
// TODO(jensj): Only put into new list of there's metadata.
List<_Chunk> result =
new List<_Chunk>.filled(chunks.length, null, growable: true);
List<_MetadataChunk> metadataChunks;
int outSize = 0;
for (_Chunk chunk in chunks) {
if (chunk is _MetadataChunk) {
metadataChunks ??= new List<_MetadataChunk>();
metadataChunks.add(chunk);
} else {
chunk.metadata = metadataChunks;
metadataChunks = null;
chunk.internalMergeAndSort();
result[outSize++] = chunk;
}
}
if (metadataChunks != null) {
for (_MetadataChunk metadata in metadataChunks) {
result[outSize++] = metadata;
}
}
result.length = outSize;
result.sort();
return result;
}
/// Parses a chunk of tokens and returns the next - unparsed - token or null
/// on error.
Token _textualizeTokens(
TextualOutlineListener listener,
Token token,
_UnknownTokenBuilder currentUnknown,
List<_Chunk> parsedChunks,
BoxedInt originalPosition) {
Token classEndToken = listener.classStartToFinish[token];
if (classEndToken != null) {
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
_ClassChunk classChunk =
new _ClassChunk(originalPosition.value++, token, classEndToken);
parsedChunks.add(classChunk);
return _textualizeClass(listener, classChunk, originalPosition);
}
Token isImportExportEndToken = listener.importExportsStartToFinish[token];
if (isImportExportEndToken != null) {
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
_ImportExportChunk importExportChunk =
new _ImportExportChunk(originalPosition.value++);
parsedChunks.add(importExportChunk);
return _textualizeImportExports(listener, token, importExportChunk);
}
Token isKnownUnsortableEndToken =
listener.unsortableElementStartToFinish[token];
if (isKnownUnsortableEndToken != null) {
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
Token beginToken = token;
parsedChunks.add(new _KnownUnsortableChunk(
originalPosition.value++, beginToken, isKnownUnsortableEndToken));
return isKnownUnsortableEndToken.next;
}
Token elementEndToken = listener.elementStartToFinish[token];
if (elementEndToken != null) {
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
Token beginToken = token;
parsedChunks.add(new _ProcedureEtcChunk(originalPosition.value++,
beginToken, elementEndToken, listener.nonClassEndOffsets));
return elementEndToken.next;
}
Token metadataEndToken = listener.metadataStartToFinish[token];
if (metadataEndToken != null) {
outputUnknownChunk(currentUnknown, parsedChunks, originalPosition);
Token beginToken = token;
parsedChunks.add(new _MetadataChunk(
originalPosition.value++, beginToken, metadataEndToken));
return metadataEndToken.next;
}
// This token --- and whatever else tokens until we reach a start token we
// know is an unknown chunk. We don't yet know the end.
if (currentUnknown.start == null) {
// Start of unknown chunk.
currentUnknown.start = token;
currentUnknown.interimEnd = token;
} else {
// Continued unknown chunk.
currentUnknown.interimEnd = token;
}
return token.next;
}
Token _textualizeImportExports(TextualOutlineListener listener, Token token,
_ImportExportChunk importExportChunk) {
int originalPosition = 0;
Token endToken = listener.importExportsStartToFinish[token];
while (endToken != null) {
importExportChunk.content
.add(new _SingleImportExportChunk(originalPosition++, token, endToken));
token = endToken.next;
endToken = listener.importExportsStartToFinish[token];
}
return token;
}
Token _textualizeClass(TextualOutlineListener listener, _ClassChunk classChunk,
BoxedInt originalPosition) {
Token token = classChunk.startToken;
// Class header.
while (token != classChunk.endToken) {
if (token.endGroup == classChunk.endToken) {
break;
}
token = token.next;
}
classChunk.headerEnd = token;
if (token == classChunk.endToken) {
// This for instance happens on named mixins, e.g.
// class C<T> = Object with A<Function(T)>;
// or when the class has no content, e.g.
// class C { }
// either way, output the end token right away to avoid a weird line break.
} else {
token = token.next;
// "Normal" class with (possibly) content.
_UnknownTokenBuilder currentUnknown = new _UnknownTokenBuilder();
while (token != classChunk.endToken) {
token = _textualizeTokens(listener, token, currentUnknown,
classChunk.content, originalPosition);
}
outputUnknownChunk(currentUnknown, classChunk.content, originalPosition);
classChunk.footerStart = classChunk.endToken;
}
return classChunk.endToken.next;
}
/// Outputs an unknown chunk if one has been started.
///
/// Resets the given builder.
void outputUnknownChunk(_UnknownTokenBuilder _currentUnknown,
List<_Chunk> parsedChunks, BoxedInt originalPosition) {
if (_currentUnknown.start == null) return;
parsedChunks.add(new _UnknownChunk(
originalPosition.value++,
_currentUnknown.start,
_currentUnknown.interimEnd,
));
_currentUnknown.start = null;
_currentUnknown.interimEnd = null;
}
main(List<String> args) {
File f = new File(args[0]);
Uint8List data = f.readAsBytesSync();
String outline =
textualOutline(data, throwOnUnexpected: true, performModelling: true);
if (args.length > 1 && args[1] == "--overwrite") {
f.writeAsStringSync(outline);
} else if (args.length > 1 && args[1] == "--benchmark") {
Stopwatch stopwatch = new Stopwatch()..start();
for (int i = 0; i < 100; i++) {
String outline2 =
textualOutline(data, throwOnUnexpected: true, performModelling: true);
if (outline2 != outline) throw "Not the same result every time";
}
stopwatch.stop();
print("First 100 took ${stopwatch.elapsedMilliseconds} ms");
stopwatch = new Stopwatch()..start();
for (int i = 0; i < 10000; i++) {
String outline2 =
textualOutline(data, throwOnUnexpected: true, performModelling: true);
if (outline2 != outline) throw "Not the same result every time";
}
stopwatch.stop();
print("Next 10,000 took ${stopwatch.elapsedMilliseconds} ms");
} else {
print(outline);
}
}
class TextualOutlineListener extends DirectiveListener {
Set<int> nonClassEndOffsets = new Set<int>();
Map<Token, Token> classStartToFinish = {};
Map<Token, Token> elementStartToFinish = {};
Map<Token, Token> metadataStartToFinish = {};
Map<Token, Token> importExportsStartToFinish = {};
Map<Token, Token> unsortableElementStartToFinish = {};
@override
void endClassMethod(Token getOrSet, Token beginToken, Token beginParam,
Token beginInitializers, Token endToken) {
nonClassEndOffsets.add(endToken.offset);
elementStartToFinish[beginToken] = endToken;
}
@override
void endTopLevelMethod(Token beginToken, Token getOrSet, Token endToken) {
nonClassEndOffsets.add(endToken.offset);
elementStartToFinish[beginToken] = endToken;
}
@override
void endClassFactoryMethod(
Token beginToken, Token factoryKeyword, Token endToken) {
nonClassEndOffsets.add(endToken.offset);
elementStartToFinish[beginToken] = endToken;
}
@override
void handleNativeFunctionBodySkipped(Token nativeToken, Token semicolon) {
// Allow native functions.
}
@override
void endClassFields(
Token abstractToken,
Token externalToken,
Token staticToken,
Token covariantToken,
Token lateToken,
Token varFinalOrConst,
int count,
Token beginToken,
Token endToken) {
elementStartToFinish[beginToken] = endToken;
}
@override
void endTopLevelFields(
Token externalToken,
Token staticToken,
Token covariantToken,
Token lateToken,
Token varFinalOrConst,
int count,
Token beginToken,
Token endToken) {
elementStartToFinish[beginToken] = endToken;
}
void endFunctionTypeAlias(
Token typedefKeyword, Token equals, Token endToken) {
elementStartToFinish[typedefKeyword] = endToken;
}
void endEnum(Token enumKeyword, Token leftBrace, int count) {
elementStartToFinish[enumKeyword] = leftBrace.endGroup;
}
@override
void endLibraryName(Token libraryKeyword, Token semicolon) {
unsortableElementStartToFinish[libraryKeyword] = semicolon;
}
@override
void endPart(Token partKeyword, Token semicolon) {
unsortableElementStartToFinish[partKeyword] = semicolon;
}
@override
void endPartOf(
Token partKeyword, Token ofKeyword, Token semicolon, bool hasName) {
unsortableElementStartToFinish[partKeyword] = semicolon;
}
@override
void endMetadata(Token beginToken, Token periodBeforeName, Token endToken) {
// Metadata's endToken is the one *after* the actual end of the metadata.
metadataStartToFinish[beginToken] = endToken.previous;
}
@override
void endClassDeclaration(Token beginToken, Token endToken) {
classStartToFinish[beginToken] = endToken;
}
@override
void endMixinDeclaration(Token mixinKeyword, Token endToken) {
classStartToFinish[mixinKeyword] = endToken;
}
@override
void endExtensionDeclaration(
Token extensionKeyword, Token onKeyword, Token endToken) {
classStartToFinish[extensionKeyword] = endToken;
}
@override
void endNamedMixinApplication(Token beginToken, Token classKeyword,
Token equals, Token implementsKeyword, Token endToken) {
classStartToFinish[beginToken] = endToken;
}
@override
void endImport(Token importKeyword, Token semicolon) {
importExportsStartToFinish[importKeyword] = semicolon;
}
@override
void endExport(Token exportKeyword, Token semicolon) {
importExportsStartToFinish[exportKeyword] = semicolon;
}
}