blob: 74ddc7620119e224636e49898143cda41e85a8a6 [file] [log] [blame]
// Copyright (c) 2024, 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 'package:_fe_analyzer_shared/src/messages/codes.dart';
import 'package:_fe_analyzer_shared/src/parser/parser.dart';
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart';
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
abstract class CodeOptimizer {
/// Returns names exported from the library [uriStr].
Set<String> getImportedNames(String uriStr);
List<Edit> optimize(
String code, {
required Set<String> libraryDeclarationNames,
required ScannerConfiguration scannerConfiguration,
bool throwIfHasErrors = false,
}) {
List<Edit> edits = [];
ScannerResult result = scanString(
code,
configuration: scannerConfiguration,
includeComments: true,
languageVersionChanged: (scanner, languageVersion) {
throw new UnimplementedError();
},
);
if (result.hasErrors) {
if (throwIfHasErrors) {
throw new StateError('Has scan errors');
}
return [];
}
_Listener listener = new _Listener(
getImportedNames: getImportedNames,
);
try {
new Parser(
listener,
allowPatterns: true,
).parseUnit(result.tokens);
} on _StateError {
// Recover by doing nothing.
return [];
}
if (listener.hasErrors) {
if (throwIfHasErrors) {
throw new StateError('Has parse errors');
}
return [];
}
List<_Import> imports = listener.importScope.imports;
for (_Import import in imports) {
for (_PrefixedName prefixedName in import.prefixedNames) {
String name = prefixedName.name.lexeme;
// If there is more than one import that exports the name.
if (!listener.importScope.hasUniqueImport(name)) {
import.namesWithPrefix.add(name);
continue;
}
// Might be shadowed by a library declaration.
if (libraryDeclarationNames.contains(name)) {
import.namesWithPrefix.add(name);
continue;
}
// Might be shadowed by a local declaration.
if (listener.declaredNames.contains(name)) {
import.namesWithPrefix.add(name);
continue;
}
// Might shadow super declaration.
if (listener.unqualifiedNames.contains(name)) {
import.namesWithPrefix.add(name);
continue;
}
import.namesWithoutPrefix.add(name);
int prefixOffset = prefixedName.prefix.offset;
edits.add(
new RemoveImportPrefixReferenceEdit(
offset: prefixOffset,
length: prefixedName.name.offset - prefixOffset,
),
);
}
}
for (_Import import in imports) {
if (import.namesWithPrefix.isEmpty) {
int uriEnd = import.uriToken.end;
edits.add(
new RemoveImportPrefixDeclarationEdit(
offset: uriEnd,
length: import.semicolon.offset - uriEnd,
),
);
} else if (import.namesWithoutPrefix.isNotEmpty) {
// If some names require the prefix, and some not, add a new import
// without a prefix, but hide those which require prefix.
List<String> namesToHide = import.namesWithPrefix.toList();
namesToHide.sort();
edits.add(
new ImportWithoutPrefixEdit(
offset: import.semicolon.end,
uriStr: import.uriStr,
namesToHide: namesToHide,
),
);
}
}
edits.sort((a, b) => a.offset - b.offset);
return edits;
}
}
sealed class Edit {
final int offset;
final int length;
final String replacement;
Edit({
required this.offset,
required this.length,
required this.replacement,
});
static String applyList(List<Edit> edits, String value) {
final StringBuffer buffer = new StringBuffer();
int offset = 0;
for (Edit edit in edits) {
buffer.write(value.substring(offset, edit.offset));
buffer.write(edit.replacement);
offset = edit.offset + edit.length;
}
if (offset < value.length) buffer.write(value.substring(offset));
return buffer.toString();
}
}
final class ImportWithoutPrefixEdit extends Edit {
final String uriStr;
final List<String> namesToHide;
ImportWithoutPrefixEdit({
required super.offset,
required this.uriStr,
required this.namesToHide,
}) : super(
length: 0,
replacement: '\nimport \'$uriStr\' hide ${namesToHide.join(', ')};',
);
}
final class RemoveDartCoreImportEdit extends RemoveEdit {
RemoveDartCoreImportEdit({
required super.offset,
required super.length,
});
}
sealed class RemoveEdit extends Edit {
RemoveEdit({
required super.offset,
required super.length,
}) : super(replacement: '');
}
final class RemoveImportPrefixDeclarationEdit extends RemoveEdit {
RemoveImportPrefixDeclarationEdit({
required super.offset,
required super.length,
});
}
final class RemoveImportPrefixReferenceEdit extends RemoveEdit {
RemoveImportPrefixReferenceEdit({
required super.offset,
required super.length,
});
}
class _Import {
final Token importKeyword;
final Token uriToken;
final String uriStr;
final _ImportPrefix prefix;
final Set<String> names;
final Token semicolon;
final List<_PrefixedName> prefixedNames = [];
/// Names that are used with [prefix], but can be used without it.
final Set<String> namesWithoutPrefix = {};
/// Names that are used with [prefix], and the prefix cannot be removed.
final Set<String> namesWithPrefix = {};
_Import({
required this.importKeyword,
required this.uriToken,
required this.uriStr,
required this.prefix,
required this.names,
required this.semicolon,
});
}
class _ImportPrefix {
final Token name;
_ImportPrefix({
required this.name,
});
}
class _ImportScope {
final List<_Import> imports = [];
_ImportScope();
void addPrefixedName(_PrefixedName prefixed) {
for (_Import import in imports) {
if (import.prefix.name.lexeme == prefixed.prefix.lexeme) {
import.prefixedNames.add(prefixed);
}
}
}
bool hasUniqueImport(String name) {
int importCount = 0;
for (_Import import in imports) {
if (import.names.contains(name)) {
importCount++;
}
}
return importCount == 1;
}
}
class _Listener extends Listener {
Set<String> Function(String uriStr) getImportedNames;
bool hasErrors = false;
_ImportScope importScope = new _ImportScope();
/// The names of local declarations.
final Set<String> declaredNames = {};
/// The names that are referenced without a preceding `<something>.`.
/// These can be references to super declarations.
final Set<String> unqualifiedNames = {};
final List<Object?> stack = [];
_Listener({
required this.getImportedNames,
});
@override
void beginExtensionDeclaration(
Token? augmentToken, Token extensionKeyword, Token? name) {
if (name != null) {
declaredNames.add(name.lexeme);
}
}
@override
void beginExtensionTypeDeclaration(
Token? augmentToken, Token extensionKeyword, Token name) {
declaredNames.add(name.lexeme);
}
@override
void endBinaryExpression(Token token, Token endToken) {
Token? prefixToken = token.previous;
if (prefixToken == null || prefixToken.type != TokenType.IDENTIFIER) {
return;
}
Token? nameToken = token.next;
if (nameToken == null || nameToken.type != TokenType.IDENTIFIER) {
return;
}
importScope.addPrefixedName(
new _PrefixedName(
prefix: prefixToken,
name: nameToken,
),
);
}
@override
void endImport(Token importKeyword, Token? augmentToken, Token? semicolon) {
_ImportPrefix prefix = popOrThrow();
Token? uriToken = importKeyword.next;
if (uriToken == null) {
throw new _StateError();
}
String uriStr = uriToken.lexeme;
if (uriStr.startsWith('\'') && uriStr.endsWith('\'')) {
uriStr = uriStr.substring(1, uriStr.length - 1);
} else {
throw new _StateError();
}
importScope.imports.add(
new _Import(
importKeyword: importKeyword,
uriToken: uriToken,
uriStr: uriStr,
prefix: prefix,
semicolon: semicolon!,
names: getImportedNames(uriStr),
),
);
}
@override
void endMetadata(Token beginToken, Token? periodBeforeName, Token endToken) {
if (beginToken.type != TokenType.AT) {
throw new _StateError();
}
Token? prefixToken = beginToken.next;
if (prefixToken == null || prefixToken.type != TokenType.IDENTIFIER) {
throw new _StateError();
}
Token? periodToken = prefixToken.next;
if (periodToken == null || periodToken.type != TokenType.PERIOD) {
return;
}
Token? nameToken = periodToken.next;
if (nameToken == null || nameToken.type != TokenType.IDENTIFIER) {
throw new _StateError();
}
importScope.addPrefixedName(
new _PrefixedName(
prefix: prefixToken,
name: nameToken,
),
);
}
@override
void handleIdentifier(Token token, IdentifierContext context) {
if (context.inDeclaration) {
declaredNames.add(token.lexeme);
}
if (context == IdentifierContext.importPrefixDeclaration) {
push(
new _ImportPrefix(
name: token,
),
);
}
}
@override
void handleRecoverableError(
Message message,
Token startToken,
Token endToken,
) {
hasErrors = true;
}
@override
void handleSend(Token beginToken, Token endToken) {
if (beginToken.type != TokenType.IDENTIFIER) {
return;
}
// If not qualified with another identifier, or expression, then it could
// be an invocation of a method from a superclass. So, we cannot remove
// the prefix from the import that provides this name, imported names
// shadow super names.
Token? period = beginToken.previous;
if (period == null || period.type != TokenType.PERIOD) {
unqualifiedNames.add(beginToken.lexeme);
}
}
@override
void handleType(Token beginToken, Token? questionMark) {
Token prefixToken = beginToken;
if (prefixToken.type != TokenType.IDENTIFIER) {
throw new _StateError();
}
Token? periodToken = prefixToken.next;
if (periodToken == null || periodToken.type != TokenType.PERIOD) {
return;
}
Token? nameToken = periodToken.next;
if (nameToken == null || nameToken.type != TokenType.IDENTIFIER) {
throw new _StateError();
}
importScope.addPrefixedName(
new _PrefixedName(
prefix: prefixToken,
name: nameToken,
),
);
}
T popOrThrow<T>() {
if (stack.lastOrNull case T last) {
stack.removeLast();
return last;
}
throw new _StateError();
}
void push(Object? value) {
stack.add(value);
}
}
class _PrefixedName {
final Token prefix;
final Token name;
_PrefixedName({
required this.prefix,
required this.name,
});
@override
String toString() {
return '$prefix.$name';
}
}
/// The exception that is thrown if an unexpected syntax found.
class _StateError {}