blob: 11842f0770c114990744048ffe9c27db1e09b074 [file] [log] [blame]
// Copyright (c) 2023, 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/macros/code_optimizer.dart' as macro;
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart' as ast;
import 'package:analyzer/src/dart/ast/ast.dart' as ast;
import 'package:analyzer/src/dart/ast/token.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/summary2/library_builder.dart';
import 'package:analyzer/src/summary2/reference.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
/// Merges elements from [partialUnits] into [unitElement].
///
/// The [unitElement] is empty initially, but [unitNode] is not, and it is
/// built from the combined macro application results, closely but not exactly
/// corresponding to units from [partialUnits]. We use [unitNode] to update
/// offsets of the elements.
class MacroElementsMerger {
final List<LinkingUnit> partialUnits;
final Reference unitReference;
final ast.CompilationUnit unitNode;
final CompilationUnitElementImpl unitElement;
final LibraryAugmentationElementImpl augmentation;
MacroElementsMerger({
required this.partialUnits,
required this.unitReference,
required this.unitNode,
required this.unitElement,
required this.augmentation,
});
void perform({
required Function() updateConstants,
}) {
_mergeClasses();
_mergeFunctions();
_mergeUnitPropertyAccessors();
_mergeUnitVariables();
updateConstants();
_rewriteImportPrefixes();
}
void _mergeClasses() {
for (final partialUnit in partialUnits) {
final elementsToAdd = <ClassElementImpl>[];
for (final element in partialUnit.element.classes) {
final reference = element.reference!;
final containerRef = element.isAugmentation
? unitReference.getChild('@classAugmentation')
: unitReference.getChild('@class');
final existingRef = containerRef[element.name];
if (existingRef == null) {
elementsToAdd.add(element);
containerRef.addChildReference(element.name, reference);
} else {
final existingElement = existingRef.element as ClassElementImpl;
if (existingElement.augmentation == element) {
existingElement.augmentation = null;
}
_mergeInstanceChildren(existingRef, existingElement, element);
}
}
unitElement.classes = [
...unitElement.classes,
...elementsToAdd,
].toFixedList();
}
}
void _mergeFunctions() {
for (final partialUnit in partialUnits) {
final elementsToAdd = <FunctionElementImpl>[];
for (final element in partialUnit.element.functions) {
final reference = element.reference!;
final containerRef = element.isAugmentation
? unitReference.getChild('@functionAugmentation')
: unitReference.getChild('@function');
final existingRef = containerRef[element.name];
if (existingRef == null) {
elementsToAdd.add(element);
containerRef.addChildReference(element.name, reference);
} else {
final existingElement = existingRef.element as FunctionElementImpl;
if (existingElement.augmentation == element) {
existingElement.augmentation = null;
}
}
}
unitElement.functions = [
...unitElement.functions,
...elementsToAdd,
].toFixedList();
}
}
void _mergeInstanceChildren(
Reference existingRef,
InstanceElementImpl existingElement,
InstanceElementImpl newElement,
) {
for (final element in newElement.fields) {
final reference = element.reference!;
final containerRef = element.isAugmentation
? existingRef.getChild('@fieldAugmentation')
: existingRef.getChild('@field');
containerRef.addChildReference(element.name, reference);
}
existingElement.fields = [
...existingElement.fields,
...newElement.fields,
].toFixedList();
for (final element in newElement.accessors) {
final reference = element.reference!;
final containerRef = element.isGetter
? element.isAugmentation
? existingRef.getChild('@getterAugmentation')
: existingRef.getChild('@getter')
: element.isAugmentation
? existingRef.getChild('@setterAugmentation')
: existingRef.getChild('@setter');
containerRef.addChildReference(element.name, reference);
}
existingElement.accessors = [
...existingElement.accessors,
...newElement.accessors,
].toFixedList();
for (final element in newElement.methods) {
final reference = element.reference!;
final containerRef = element.isAugmentation
? existingRef.getChild('@methodAugmentation')
: existingRef.getChild('@method');
containerRef.addChildReference(element.name, reference);
}
existingElement.methods = [
...existingElement.methods,
...newElement.methods,
].toFixedList();
if (existingElement is InterfaceElementImpl &&
newElement is InterfaceElementImpl) {
if (newElement.interfaces.isNotEmpty) {
existingElement.interfaces = [
...existingElement.interfaces,
...newElement.interfaces,
].toFixedList();
}
for (final element in newElement.constructors) {
final reference = element.reference!;
final containerRef = element.isAugmentation
? existingRef.getChild('@constructorAugmentation')
: existingRef.getChild('@constructor');
containerRef.addChildReference(element.name, reference);
}
existingElement.constructors = [
...existingElement.constructors,
...newElement.constructors,
].toFixedList();
}
}
void _mergeUnitPropertyAccessors() {
final containerRef = unitReference.getChild('@accessor');
for (final partialUnit in partialUnits) {
for (final element in partialUnit.element.accessors) {
final reference = element.reference!;
containerRef.addChildReference(element.name, reference);
}
unitElement.accessors = [
...unitElement.accessors,
...partialUnit.element.accessors,
].toFixedList();
}
}
void _mergeUnitVariables() {
final containerRef = unitReference.getChild('@topLevelVariable');
for (final partialUnit in partialUnits) {
for (final element in partialUnit.element.topLevelVariables) {
final reference = element.reference!;
containerRef.addChildReference(element.name, reference);
}
unitElement.topLevelVariables = [
...unitElement.topLevelVariables,
...partialUnit.element.topLevelVariables,
].toFixedList();
}
}
void _rewriteImportPrefixes() {
final uriToPartialPrefixes = <Uri, List<PrefixElementImpl>>{};
for (final partialUnit in partialUnits) {
for (final import in partialUnit.container.libraryImports) {
final prefix = import.prefix?.element;
final importedLibrary = import.importedLibrary;
if (prefix != null && importedLibrary != null) {
final uri = importedLibrary.source.uri;
(uriToPartialPrefixes[uri] ??= []).add(prefix);
}
}
}
// The merged augmentation imports the same libraries, but with
// different prefixes. Prepare the mapping.
final partialPrefixToMerged =
Map<PrefixElementImpl, PrefixElementImpl>.identity();
for (final import in augmentation.libraryImports) {
final prefix = import.prefix?.element;
final importedLibrary = import.importedLibrary;
if (prefix != null && importedLibrary != null) {
final uri = importedLibrary.source.uri;
final partialPrefixes = uriToPartialPrefixes[uri];
if (partialPrefixes != null) {
for (final partialPrefix in partialPrefixes) {
partialPrefixToMerged[partialPrefix] = prefix;
}
}
}
}
// Rewrite import prefixes in constants.
final visitor = _RewriteImportPrefixes(partialPrefixToMerged);
for (final partialUnit in partialUnits) {
partialUnit.node.accept(visitor);
}
}
}
class MacroUpdateConstantsForOptimizedCode {
/// The container of [unitElement].
final LibraryElementImpl libraryElement;
/// The parsed merged code.
final ast.CompilationUnit unitNode;
/// The edits applied to the code that was parsed into [unitNode].
/// This class does not have the optimized code itself.
final List<macro.Edit> codeEdits;
/// The merged element, with elements in the same order as in [unitNode].
final CompilationUnitElementImpl unitElement;
/// The names of classes that have a `const` constructor.
final Set<String> _namesOfConstClasses = {};
MacroUpdateConstantsForOptimizedCode({
required this.libraryElement,
required this.unitNode,
required this.codeEdits,
required this.unitElement,
});
/// Iterate over two lists of annotations and constants:
///
/// 1. From the merged, but not optimized, parsed AST.
///
/// 2. From the merged element models, also not optimized.
/// Because we never optimize partial macro units.
///
/// These elements are *not* produced from the parsed AST above.
/// But they must be fully consistent with each other.
/// The same elements, in the same order.
/// If not, we have a bug in [MacroElementsMerger].
void perform() {
_findConstClasses();
var nodeRecords = _orderedForNodes();
var elementRecords = _orderedForElement();
assert(nodeRecords.length == elementRecords.length);
for (var i = 0; i < nodeRecords.length; i++) {
var nodeRecord = nodeRecords[i];
var nodeTokens = nodeRecord.$2.allTokens;
var elementRecord = elementRecords[i];
var elementAnnotation = elementRecord.$2;
var elementTokens = elementAnnotation.allTokens;
assert(nodeTokens.length == elementTokens.length);
elementAnnotation.accept(
_RemoveImportPrefixesVisitor(
codeEdits: codeEdits,
nodeTokens: nodeTokens,
elementTokens: elementTokens.asElementToIndexMap,
onReplace: ({required toReplace, required replacement}) {
// Maybe replace the whole constant initializer.
if (elementRecord.$1 case ConstVariableElement element) {
if (element.constantInitializer == toReplace) {
replacement as ast.ExpressionImpl;
element.constantInitializer = replacement;
}
}
},
),
);
}
}
void _findConstClasses() {
for (var element in libraryElement.topLevelElements) {
if (element is! ClassElementImpl) continue;
if (element.isAugmentation) continue;
var augmented = element.augmented;
if (augmented == null) continue;
var hasConst = augmented.constructors.any((e) => e.isConst);
if (hasConst) {
_namesOfConstClasses.add(element.name);
}
}
}
List<(ElementImpl, ast.AstNodeImpl)> _orderedForElement() {
var result = <(ElementImpl, ast.AstNodeImpl)>[];
void addElement(ElementImpl element) {
for (var annotation in element.metadata) {
result.add((element, annotation.annotationAst));
}
switch (element) {
case ConstVariableElement():
if (element.constantInitializer case var initializer?) {
result.add((element, initializer));
}
case ExecutableElementImpl():
for (var formalParameter in element.parameters) {
addElement(formalParameter.declarationImpl);
}
}
}
void addInstanceElement(InstanceElementImpl element) {
addElement(element);
for (var field in element.fields) {
if (!field.isSynthetic) {
addElement(field);
}
}
for (var getter in element.accessors) {
if (getter.isGetter && !getter.isSynthetic) {
addElement(getter);
}
}
for (var setter in element.accessors) {
if (setter.isSetter && !setter.isSynthetic) {
addElement(setter);
}
}
for (var method in element.methods) {
addElement(method);
}
}
void addInterfaceElement(InterfaceElementImpl element) {
addInstanceElement(element);
for (var constructor in element.constructors) {
addElement(constructor);
}
}
for (var class_ in unitElement.classes) {
addInterfaceElement(class_);
}
for (var variable in unitElement.topLevelVariables) {
if (!variable.isSynthetic) {
addElement(variable);
}
}
for (var getter in unitElement.accessors) {
if (getter.isGetter && !getter.isSynthetic) {
addElement(getter);
}
}
for (var setter in unitElement.accessors) {
if (setter.isSetter && !setter.isSynthetic) {
addElement(setter);
}
}
for (var function in unitElement.functions) {
addElement(function);
}
return result;
}
List<(ast.AstNodeImpl, ast.AstNodeImpl)> _orderedForNodes() {
var result = <(ast.AstNodeImpl, ast.AstNodeImpl)>[];
void addMetadata(
ast.AstNodeImpl node,
List<ast.AnnotationImpl> metadata,
) {
for (var annotation in metadata) {
result.add((node, annotation));
}
}
void addAnnotatedNode(ast.AnnotatedNodeImpl node) {
addMetadata(node, node.metadata);
}
void addVariableList(
ast.VariableDeclarationListImpl variableList,
List<ast.AnnotationImpl> metadata, {
required bool withFinals,
}) {
for (var variable in variableList.variables) {
addMetadata(variable, metadata);
if (variableList.isConst || variableList.isFinal && withFinals) {
if (variable.initializer case var initializer?) {
result.add((variable, initializer));
}
}
}
}
void addFormalParameters(ast.FormalParameterListImpl? parameterList) {
if (parameterList != null) {
for (var formalParameter in parameterList.parameters) {
addMetadata(formalParameter, formalParameter.metadata);
if (formalParameter is ast.DefaultFormalParameterImpl) {
if (formalParameter.defaultValue case var defaultValue?) {
result.add((formalParameter, defaultValue));
}
}
}
}
}
void addInterfaceMembers(
List<ast.ClassMemberImpl> members, {
required bool hasConstConstructor,
}) {
for (var field in members) {
if (field is ast.FieldDeclarationImpl) {
addVariableList(
field.fields,
field.metadata,
withFinals: hasConstConstructor && !field.isStatic,
);
}
}
for (var getter in members) {
if (getter is ast.MethodDeclarationImpl && getter.isGetter) {
addAnnotatedNode(getter);
addFormalParameters(getter.parameters);
}
}
for (var setter in members) {
if (setter is ast.MethodDeclarationImpl && setter.isSetter) {
addAnnotatedNode(setter);
addFormalParameters(setter.parameters);
}
}
for (var method in members) {
if (method is ast.MethodDeclarationImpl &&
method.propertyKeyword == null) {
addAnnotatedNode(method);
addFormalParameters(method.parameters);
}
}
for (var constructor in members) {
if (constructor is ast.ConstructorDeclarationImpl) {
addAnnotatedNode(constructor);
addFormalParameters(constructor.parameters);
}
}
}
for (var class_ in unitNode.declarations) {
if (class_ is ast.ClassDeclarationImpl) {
addAnnotatedNode(class_);
addInterfaceMembers(
class_.members,
hasConstConstructor: _namesOfConstClasses.contains(
class_.name.lexeme,
),
);
}
}
for (var topVariable in unitNode.declarations) {
if (topVariable is ast.TopLevelVariableDeclarationImpl) {
addVariableList(
topVariable.variables,
topVariable.metadata,
withFinals: false,
);
}
}
for (var getter in unitNode.declarations) {
if (getter is ast.FunctionDeclarationImpl && getter.isGetter) {
addAnnotatedNode(getter);
addFormalParameters(getter.functionExpression.parameters);
}
}
for (var setter in unitNode.declarations) {
if (setter is ast.FunctionDeclarationImpl && setter.isSetter) {
addAnnotatedNode(setter);
addFormalParameters(setter.functionExpression.parameters);
}
}
for (var function in unitNode.declarations) {
if (function is ast.FunctionDeclarationImpl &&
function.propertyKeyword == null) {
addAnnotatedNode(function);
addFormalParameters(function.functionExpression.parameters);
}
}
return result;
}
}
class _RemoveImportPrefixesVisitor extends ast.RecursiveAstVisitor<void> {
final List<macro.Edit> codeEdits;
final List<Token> nodeTokens;
final Map<Token, int> elementTokens;
final void Function({
required ast.AstNodeImpl toReplace,
required ast.AstNodeImpl replacement,
}) onReplace;
_RemoveImportPrefixesVisitor({
required this.codeEdits,
required this.nodeTokens,
required this.elementTokens,
required this.onReplace,
});
@override
void visitNamedType(covariant ast.NamedTypeImpl node) {
if (node.importPrefix case var importPrefix?) {
var prefix = _correspondingNodeToken(importPrefix.name);
var edit = _editForRemovePrefix(prefix);
if (edit != null) {
node.importPrefix = null;
}
}
super.visitNamedType(node);
}
@override
void visitPrefixedIdentifier(covariant ast.PrefixedIdentifierImpl node) {
var prefix = _correspondingNodeToken(node.prefix.token);
var edit = _editForRemovePrefix(prefix);
if (edit != null) {
NodeReplacer.replace(node, node.identifier);
onReplace(
toReplace: node,
replacement: node.identifier,
);
}
super.visitPrefixedIdentifier(node);
}
Token _correspondingNodeToken(Token elementToken) {
var index = elementTokens[elementToken]!;
return nodeTokens[index];
}
macro.RemoveImportPrefixReferenceEdit? _editForRemovePrefix(Token prefix) {
for (var edit in codeEdits) {
if (edit is macro.RemoveImportPrefixReferenceEdit) {
if (edit.offset == prefix.offset) {
return edit;
}
}
}
return null;
}
}
class _RewriteImportPrefixes extends ast.RecursiveAstVisitor<void> {
final Map<PrefixElementImpl, PrefixElementImpl> partialPrefixToMerged;
_RewriteImportPrefixes(this.partialPrefixToMerged);
@override
void visitSimpleIdentifier(covariant ast.SimpleIdentifierImpl node) {
final mergedPrefix = partialPrefixToMerged[node.staticElement];
if (mergedPrefix != null) {
// The name may be different in the merged augmentation.
node.token = StringToken(
TokenType.IDENTIFIER,
mergedPrefix.name,
-1,
);
// The element is definitely different.
node.staticElement = mergedPrefix;
}
}
}