blob: 2497648575478b5e2633fe3b5c2c1146be1ef516 [file] [log] [blame]
// Copyright (c) 2022, 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/element/element.dart'; // ignore: implementation_imports
import 'package:analyzer/src/dart/element/type.dart' // ignore: implementation_imports
show InvalidTypeImpl;
import 'package:collection/collection.dart';
import 'util/dart_type_utilities.dart';
class EnumLikeClassDescription {
final Map<DartObject, Set<FieldElement>> _enumConstants;
EnumLikeClassDescription(this._enumConstants);
/// Returns a fresh map of the class's enum-like constant values.
Map<DartObject, Set<FieldElement>> get enumConstants => {..._enumConstants};
}
extension AstNodeExtension on AstNode {
Iterable<AstNode> get childNodes => childEntities.whereType<AstNode>();
/// Whether this is the child of a private compilation unit member.
bool get inPrivateMember {
var parent = this.parent;
return switch (parent) {
NamedCompilationUnitMember() => parent.name.isPrivate,
ExtensionDeclaration() => parent.name == null || parent.name.isPrivate,
_ => false,
};
}
bool get isAugmentation {
var self = this;
return switch (self) {
ClassDeclaration() => self.augmentKeyword != null,
ConstructorDeclaration() => self.augmentKeyword != null,
EnumConstantDeclaration() => self.augmentKeyword != null,
EnumDeclaration() => self.augmentKeyword != null,
ExtensionTypeDeclaration() => self.augmentKeyword != null,
FieldDeclaration() => self.augmentKeyword != null,
FunctionDeclaration() => self.augmentKeyword != null,
FunctionExpression() => self.parent?.isAugmentation ?? false,
MethodDeclaration() => self.augmentKeyword != null,
MixinDeclaration() => self.augmentKeyword != null,
TopLevelVariableDeclaration() => self.augmentKeyword != null,
VariableDeclaration(declaredFragment: var fragment?) =>
fragment is PropertyInducingFragment && fragment.isAugmentation,
_ => false,
};
}
bool get isEffectivelyPrivate {
var node = this;
if (node.isInternal) return true;
if (node is ClassDeclaration) {
var classElement = node.declaredFragment?.element;
if (classElement != null) {
if (classElement.isSealed) return true;
if (classElement.isAbstract) {
if (classElement.isFinal) return true;
if (classElement.isInterface) return true;
}
}
}
return false;
}
bool get isInternal {
var self = this;
if (self is VariableDeclaration) {
var element = self.declaredFragment?.element;
if (element is TopLevelVariableElement) {
return element.metadata.hasInternal;
}
}
var parent = thisOrAncestorOfType<CompilationUnitMember>();
if (parent == null) return false;
var metadata = parent.declaredFragment?.element.metadata;
return metadata?.hasInternal ?? false;
}
}
extension AstNodeNullableExtension on AstNode? {
Element? get canonicalElement {
var self = this;
if (self is Expression) {
var node = self.unParenthesized;
if (node is Identifier) {
return node.element;
} else if (node is PropertyAccess) {
return node.propertyName.element;
}
}
return null;
}
/// Whether the expression is null-aware, or if one of its recursive targets
/// is null-aware.
bool get containsNullAwareInvocationInChain {
var node = this;
if (node is PropertyAccess) {
if (node.isNullAware) return true;
return node.target.containsNullAwareInvocationInChain;
} else if (node is MethodInvocation) {
if (node.isNullAware) return true;
return node.target.containsNullAwareInvocationInChain;
} else if (node is IndexExpression) {
if (node.isNullAware) return true;
return node.target.containsNullAwareInvocationInChain;
}
return false;
}
bool get isFieldNameShortcut {
var node = this;
if (node is NullCheckPattern) node = node.parent;
if (node is NullAssertPattern) node = node.parent;
return node is PatternField && node.name != null && node.name?.name == null;
}
}
extension BlockExtension on Block {
/// The last statement of this block, or `null` if this is empty.
///
/// If the last immediate statement of this block is a [Block], recurses into
/// it to find the last statement.
Statement? get lastStatement {
if (statements.isEmpty) {
return null;
}
var lastStatement = statements.last;
if (lastStatement is Block) {
return lastStatement.lastStatement;
}
return lastStatement;
}
}
extension ClassElementExtension on ClassElement {
/// Whether this [ClassElement], or one of its supertypes, is annotated with
/// `@Immutable`.
bool get hasImmutableAnnotation => [
...allSupertypes.map((t) => t.element),
this,
].any((e) => e.metadata.hasImmutable);
bool get _hasSubclassInDefiningCompilationUnit {
for (var cls in library.classes) {
InterfaceType? classType = cls.thisType;
do {
classType = classType?.superclass;
if (classType == thisType) {
return true;
}
} while (classType != null && !classType.isDartCoreObject);
}
return false;
}
/// Returns an [EnumLikeClassDescription] for this if the latter is a valid
/// "enum-like" class.
///
/// An enum-like class must meet the following requirements:
///
/// * is concrete,
/// * has no public constructors,
/// * has no factory constructors,
/// * has two or more static const fields with the same type as the class,
/// * has no subclasses declared in the defining library.
///
/// The returned [EnumLikeClassDescription]'s `enumConstantNames` contains all
/// of the static const fields with the same type as the class, with one
/// exception; any static const field which is marked `@Deprecated` and is
/// equal to another static const field with the same type as the class is not
/// included. Such a field is assumed to be deprecated in favor of the field
/// with equal value.
EnumLikeClassDescription? asEnumLikeClass() {
// See discussion: https://github.com/dart-lang/linter/issues/2083.
// Must be concrete.
if (isAbstract) {
return null;
}
// With only private non-factory constructors.
for (var constructor in constructors) {
if (!constructor.isPrivate || constructor.isFactory) {
return null;
}
}
var type = thisType;
// And 2 or more static const fields whose type is the enclosing class.
var enumConstantCount = 0;
var enumConstants = <DartObject, Set<FieldElement>>{};
for (var field in fields) {
// Ensure static const.
if (field.isSynthetic || !field.isConst || !field.isStatic) {
continue;
}
// Check for type equality.
if (field.type != type) {
continue;
}
var fieldValue = field.computeConstantValue();
if (fieldValue == null) {
continue;
}
enumConstantCount++;
enumConstants.putIfAbsent(fieldValue, () => {}).add(field);
}
if (enumConstantCount < 2) {
return null;
}
// And no subclasses in the defining library.
if (_hasSubclassInDefiningCompilationUnit) return null;
return EnumLikeClassDescription(enumConstants);
}
bool isEnumLikeClass() => asEnumLikeClass() != null;
}
extension ConstructorElementExtension on ConstructorElement {
/// Whether this [ConstructorElement] is the same constructor as the
/// [className] constructor named [constructorName] declared in [uri].
bool isSameAs({
required String uri,
required String className,
required String constructorName,
}) =>
library.name == uri &&
enclosingElement.name == className &&
name == constructorName;
}
extension DartTypeExtension on DartType? {
/// Whether this [DartType] extends [className], declared in [library].
bool extendsClass(String? className, String library) {
var self = this;
return self is InterfaceType &&
_extendsClass(self, <InterfaceElement>{}, className, library);
}
/// Whether this [DartType] implements any of [definitions].
bool implementsAnyInterface(Iterable<InterfaceTypeDefinition> definitions) {
var typeToCheck = this;
if (typeToCheck is TypeParameterType) {
typeToCheck = typeToCheck.typeForInterfaceCheck;
}
if (typeToCheck is! InterfaceType) return false;
bool isAnyInterface(InterfaceType i) =>
definitions.any((d) => i.isSameAs(d.name, d.library));
return isAnyInterface(typeToCheck) ||
!typeToCheck.element.isSynthetic &&
typeToCheck.element.allSupertypes.any(isAnyInterface);
}
/// Whether this [DartType] implements [interface], declared in [library].
bool implementsInterface(String interface, String library) {
var self = this;
if (self is! InterfaceType) return false;
if (self.isSameAs(interface, library)) return true;
if (self.element.isSynthetic) return false;
return self.element.allSupertypes.any(
(i) => i.isSameAs(interface, library),
);
}
/// Whether this [DartType] is the same element as [interface], declared in
/// [library].
bool isSameAs(String? interface, String? library) {
var self = this;
return self is InterfaceType &&
self.element.name == interface &&
self.element.library.name == library;
}
static bool _extendsClass(
InterfaceType? type,
Set<InterfaceElement> seenElements,
String? className,
String? library,
) =>
type != null &&
seenElements.add(type.element) &&
(type.isSameAs(className, library) ||
_extendsClass(type.superclass, seenElements, className, library));
}
extension ElementAnnotationExtension on ElementAnnotation {
bool get isReflectiveTest => switch (element) {
GetterElement(:var name, :var library) =>
name == 'reflectiveTest' &&
library.uri.toString() ==
'package:test_reflective_loader/test_reflective_loader.dart',
_ => false,
};
}
extension ElementExtension on Element? {
Element? get canonicalElement2 => switch (this) {
PropertyAccessorElement(:var variable) => variable,
_ => this,
};
/// Whether this is annotated with `@awaitNotRequired`.
bool get hasAwaitNotRequired {
var self = this;
if (self == null) {
return false;
}
return self.metadata.hasAwaitNotRequired ||
(self is PropertyAccessorElement && self.variable.hasAwaitNotRequired);
}
bool get isDartCorePrint {
var self = this;
return self is TopLevelFunctionElement &&
self.name == 'print' &&
self.firstFragment.libraryFragment.element.isDartCore;
}
/// Returns the class member that is overridden by `this`, if there is one,
/// as defined by [InterfaceElement.getInheritedMember].
ExecutableElement? get overriddenMember {
var member = switch (this) {
FieldElement(:var getter) => getter,
MethodElement method => method,
PropertyAccessorElement accessor => accessor,
_ => null,
};
if (member == null) return null;
var interfaceElement = member.enclosingElement;
if (interfaceElement is! InterfaceElement) return null;
var name = Name.forElement(member);
if (name == null) return null;
return interfaceElement.getInheritedMember(name);
}
}
extension ExpressionExtension on Expression {
/// Returns whether `await` is not required for this expression.
bool get isAwaitNotRequired {
var element = switch (this) {
BinaryExpression(:var element) => element,
MethodInvocation(:var methodName) => methodName.element,
PrefixedIdentifier(:var identifier) => identifier.element,
PrefixExpression(:var element) => element,
PropertyAccess(:var propertyName) => propertyName.element,
_ => null,
};
if (element == null) return false;
if (element.hasAwaitNotRequired) return true;
var elementName = element.name;
if (elementName == null) return false;
var enclosingElement = element.enclosingElement;
if (enclosingElement is! InterfaceElement) return false;
var superTypes = enclosingElement.allSupertypes;
var superMembers = element is MethodElement
? superTypes.map((t) => t.getMethod(elementName))
: superTypes.map((t) => t.getGetter(elementName));
return superMembers.any((e) => e.hasAwaitNotRequired);
}
}
extension ExpressionNullableExtension on Expression? {
/// A very, very, very rough approximation of the context type of this node.
///
/// This approximation will never be accurate for some expressions.
DartType? get approximateContextType {
var self = this;
if (self == null) return null;
var ancestor = self.parent;
var ancestorChild = self;
while (ancestor != null) {
if (ancestor is ParenthesizedExpression) {
ancestorChild = ancestor;
ancestor = ancestor.parent;
} else if (ancestor is CascadeExpression &&
ancestorChild == ancestor.target) {
ancestorChild = ancestor;
ancestor = ancestor.parent;
} else {
break;
}
}
switch (ancestor) {
// TODO(srawlins): Handle [AwaitExpression], [BinaryExpression],
// [CascadeExpression], [SwitchExpressionCase], likely others. Or move
// everything here to an analysis phase which has the actual context type.
case ArgumentList():
// Allow `function(LinkedHashSet())` for `function(LinkedHashSet mySet)`
// and `function(LinkedHashMap())` for `function(LinkedHashMap myMap)`.
return self.correspondingParameter?.type ?? InvalidTypeImpl.instance;
case AssignmentExpression():
// Allow `x = LinkedHashMap()`.
return ancestor.staticType;
case ConditionalExpression():
return ancestor.staticType;
case ConstructorFieldInitializer():
var fieldElement = ancestor.fieldName.element;
return (fieldElement is VariableElement) ? fieldElement.type : null;
case ExpressionFunctionBody(parent: var function)
when function is FunctionExpression:
// Allow `<int, LinkedHashSet>{}.putIfAbsent(3, () => LinkedHashSet())`
// and `<int, LinkedHashMap>{}.putIfAbsent(3, () => LinkedHashMap())`.
var functionParent = function.parent;
if (functionParent is FunctionDeclaration) {
return functionParent.returnType?.type;
}
var functionType = function.approximateContextType;
return functionType is FunctionType ? functionType.returnType : null;
case ExpressionFunctionBody(parent: var function)
when function is FunctionDeclaration:
return function.returnType?.type;
case ExpressionFunctionBody(parent: var function)
when function is MethodDeclaration:
return function.returnType?.type;
case NamedExpression():
// Allow `void f({required LinkedHashSet<Foo> s})`.
return ancestor.correspondingParameter?.type ??
InvalidTypeImpl.instance;
case ReturnStatement():
return ancestor.thisOrAncestorOfType<FunctionBody>().expectedReturnType;
case VariableDeclaration(parent: VariableDeclarationList(:var type)):
// Allow `LinkedHashSet<int> s = node` and
// `LinkedHashMap<int> s = node`.
return type?.type;
case YieldStatement():
return ancestor.thisOrAncestorOfType<FunctionBody>().expectedReturnType;
}
return null;
}
bool get isNullLiteral => this?.unParenthesized is NullLiteral;
}
extension FieldDeclarationExtension on FieldDeclaration {
bool get isInvalidExtensionTypeField =>
!isStatic && parent is ExtensionTypeDeclaration;
}
extension FunctionBodyExtension on FunctionBody? {
/// Attempts to calculate the expected return type of the function represented
/// by this node, accounting for an approximation of the function's context
/// type, in the case of a function literal.
DartType? get expectedReturnType {
var self = this;
if (self == null) return null;
var parent = self.parent;
if (parent is FunctionExpression) {
var grandparent = parent.parent;
if (grandparent is FunctionDeclaration) {
var returnType = grandparent.declaredFragment?.element.returnType;
return self._expectedReturnableOrYieldableType(returnType);
}
var functionType = parent.approximateContextType;
if (functionType is! FunctionType) return null;
var returnType = functionType.returnType;
return self._expectedReturnableOrYieldableType(returnType);
}
if (parent is MethodDeclaration) {
var returnType = parent.declaredFragment?.element.returnType;
return self._expectedReturnableOrYieldableType(returnType);
}
return null;
}
/// Extracts the expected type for return statements or yield statements.
///
/// For example, for an asynchronous body in a function with a declared
/// [returnType] of `Future<int>`, this returns `int`. (Note: it would be more
/// accurate to use `FutureOr<int>` and an assignability check, but `int` is
/// an approximation that works for now; this should probably be revisited.)
DartType? _expectedReturnableOrYieldableType(DartType? returnType) {
var self = this;
if (self == null) return null;
if (returnType is! InterfaceType) return null;
if (self.isAsynchronous) {
if (!self.isGenerator && returnType.isDartAsyncFuture) {
return returnType.typeArguments.firstOrNull;
}
if (self.isGenerator && returnType.isDartAsyncStream) {
return returnType.typeArguments.firstOrNull;
}
} else {
if (self.isGenerator && returnType.isDartCoreIterable) {
return returnType.typeArguments.firstOrNull;
}
}
return returnType;
}
}
extension InstanceElementExtension on InstanceElement {
bool get isReflectiveTest =>
this is ClassElement &&
metadata.annotations.any((a) => a.isReflectiveTest);
}
extension InterfaceTypeExtension on InterfaceType {
/// Returns the collection of all interfaces that this type implements,
/// including itself.
Iterable<InterfaceType> get implementedInterfaces {
void searchSupertypes(
InterfaceType? type,
Set<InterfaceElement> alreadyVisited,
List<InterfaceType> interfaceTypes,
) {
if (type == null || !alreadyVisited.add(type.element)) {
return;
}
interfaceTypes.add(type);
searchSupertypes(type.superclass, alreadyVisited, interfaceTypes);
for (var interface in type.interfaces) {
searchSupertypes(interface, alreadyVisited, interfaceTypes);
}
for (var mixin in type.mixins) {
searchSupertypes(mixin, alreadyVisited, interfaceTypes);
}
}
var interfaceTypes = <InterfaceType>[];
searchSupertypes(this, {}, interfaceTypes);
return interfaceTypes;
}
GetterElement? getGetter2(String name, {LibraryElement? library}) =>
getters.firstWhereOrNull(
(s) => s.name == name && (library == null || (s.library == library)),
);
SetterElement? getSetter2(String name) =>
setters.firstWhereOrNull((s) => s.canonicalName == name);
}
extension MethodDeclarationExtension on MethodDeclaration {
bool get hasInheritedMethod => lookUpInheritedMethod() != null;
/// Returns whether this method is an override of a method in any supertype.
bool get isOverride {
var element = declaredFragment?.element;
var name = element?.name;
if (name == null) return false;
var parentElement = element?.enclosingElement;
if (parentElement is! InterfaceElement) return false;
var parentLibrary = parentElement.library;
if (isGetter) {
// Search supertypes for a getter of the same name.
return parentElement.allSupertypes.any(
(t) => t.lookUpGetter(name, parentLibrary) != null,
);
} else if (isSetter) {
// Search supertypes for a setter of the same name.
return parentElement.allSupertypes.any(
(t) => t.lookUpSetter(name, parentLibrary) != null,
);
} else {
// Search supertypes for a method of the same name.
return parentElement.allSupertypes.any(
(t) => t.lookUpMethod(name, parentLibrary) != null,
);
}
}
MethodElement? lookUpInheritedMethod() {
var declaredElement = declaredFragment?.element;
if (declaredElement != null) {
var parent = declaredElement.enclosingElement;
if (parent is InterfaceElement) {
var methodName = Name.forElement(declaredElement);
if (methodName == null) return null;
var inherited = parent.getInheritedMember(methodName);
if (inherited is InternalMethodElement) return inherited;
}
}
return null;
}
}
extension SetterElementExtension on SetterElement {
/// Return name in a format suitable for string comparison.
String? get canonicalName {
var name = this.name;
if (name == null) return null;
// TODO(pq): remove when `name3` consistently does not include a trailing `=`.
return name.endsWith('=') ? name.substring(0, name.length - 1) : name;
}
}
extension StringExtension on String {
String toAbsoluteNormalizedPath() {
var pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
return pathContext.normalize(pathContext.absolute(this));
}
}
extension TokenExtension on Token? {
bool get isFinal => this?.keyword == Keyword.FINAL;
/// Whether the given identifier has a private name.
bool get isPrivate {
var self = this;
return self != null ? Identifier.isPrivateName(self.lexeme) : false;
}
}
extension TokenTypeExtension on TokenType {
TokenType get inverted => switch (this) {
TokenType.LT_EQ => TokenType.GT_EQ,
TokenType.LT => TokenType.GT,
TokenType.GT => TokenType.LT,
TokenType.GT_EQ => TokenType.LT_EQ,
_ => this,
};
}