| // 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:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/error/listener.dart'; |
| import 'package:analyzer/src/dart/ast/ast.dart'; |
| import 'package:analyzer/src/dart/element/extensions.dart'; |
| import 'package:analyzer/src/error/codes.dart'; |
| import 'package:analyzer/src/utilities/extensions/string.dart'; |
| import 'package:analyzer/src/workspace/workspace.dart'; |
| import 'package:meta/meta_meta.dart'; |
| |
| /// Helper for verifying the validity of annotations. |
| class AnnotationVerifier { |
| final ErrorReporter _errorReporter; |
| |
| /// The current library. |
| final LibraryElement _currentLibrary; |
| |
| /// The [WorkspacePackage] in which [_currentLibrary] is declared. |
| final WorkspacePackage? _workspacePackage; |
| |
| /// Whether [_currentLibrary] is part of its containing package's public API. |
| late final bool _inPackagePublicApi = _workspacePackage != null && |
| _workspacePackage.sourceIsInPublicApi(_currentLibrary.source); |
| |
| AnnotationVerifier( |
| this._errorReporter, |
| this._currentLibrary, |
| this._workspacePackage, |
| ); |
| |
| void checkAnnotation(Annotation node) { |
| var element = node.elementAnnotation; |
| if (element == null) { |
| return; |
| } |
| var parent = node.parent; |
| if (element.isFactory) { |
| _checkFactory(node); |
| } else if (element.isImmutable) { |
| _checkImmutable(node); |
| } else if (element.isInternal) { |
| _checkInternal(node); |
| } else if (element.isLiteral) { |
| _checkLiteral(node); |
| } else if (element.isMustBeOverridden) { |
| _checkMustBeOverridden(node); |
| } else if (element.isMustCallSuper) { |
| _checkMustCallSuper(node); |
| } else if (element.isNonVirtual) { |
| _checkNonVirtual(node); |
| } else if (element.isReopen) { |
| _checkReopen(node); |
| } else if (element.isRedeclare) { |
| _checkRedeclare(node); |
| } else if (element.isSealed) { |
| _checkSealed(node); |
| } else if (element.isUseResult) { |
| _checkUseResult(node, element); |
| } else if (element.isVisibleForTemplate || |
| element.isVisibleForTesting || |
| element.isVisibleForOverriding) { |
| _checkVisibility(node, element); |
| } else if (element.isVisibleOutsideTemplate) { |
| _checkVisibility(node, element); |
| _checkVisibleOutsideTemplate(node); |
| } |
| |
| _checkKinds(node, parent, element); |
| } |
| |
| /// Reports a warning if the annotation's parent is not a valid target for a |
| /// `@factory` annotation. |
| void _checkFactory(AstNode node) { |
| var parent = node.parent; |
| if (parent is! MethodDeclaration) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_FACTORY_ANNOTATION, |
| ); |
| return; |
| } |
| var returnType = parent.returnType?.type; |
| if (returnType is VoidType) { |
| _errorReporter.atToken( |
| parent.name, |
| WarningCode.INVALID_FACTORY_METHOD_DECL, |
| arguments: [parent.name.lexeme], |
| ); |
| return; |
| } |
| |
| FunctionBody body = parent.body; |
| if (body is EmptyFunctionBody) { |
| // Abstract methods are OK. |
| return; |
| } |
| |
| // Returns `true` for expressions like `new Foo()` or `null`. |
| bool factoryExpression(Expression? expression) => |
| expression is InstanceCreationExpression || expression is NullLiteral; |
| |
| if (body is ExpressionFunctionBody && factoryExpression(body.expression)) { |
| return; |
| } else if (body is BlockFunctionBody) { |
| NodeList<Statement> statements = body.block.statements; |
| if (statements.isNotEmpty) { |
| Statement last = statements.last; |
| if (last is ReturnStatement && factoryExpression(last.expression)) { |
| return; |
| } |
| } |
| } |
| |
| _errorReporter.atToken( |
| parent.name, |
| WarningCode.INVALID_FACTORY_METHOD_IMPL, |
| arguments: [parent.name.lexeme], |
| ); |
| } |
| |
| /// Reports a warning at [node] if it's parent is not a valid target for an |
| /// `@immutable` annotation. |
| void _checkImmutable(AstNode node) { |
| // TODO(srawlins): Switch this annotation to use `TargetKinds`. |
| var parent = node.parent; |
| if (parent is! ClassDeclaration && |
| parent is! ClassTypeAlias && |
| parent is! ExtensionTypeDeclaration && |
| parent is! MixinDeclaration) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_IMMUTABLE_ANNOTATION, |
| ); |
| } |
| } |
| |
| /// Reports a warning at [node] if it's parent is not a valid target for an |
| /// `@internal` annotation. |
| void _checkInternal(AstNode node) { |
| var parent = node.parent; |
| var parentElement = parent is Declaration ? parent.declaredElement : null; |
| var parentElementIsPrivate = parentElement?.isPrivate ?? false; |
| if (parent is TopLevelVariableDeclaration) { |
| for (var variable in parent.variables.variables) { |
| var element = variable.declaredElement as TopLevelVariableElement; |
| if (Identifier.isPrivateName(element.name)) { |
| _errorReporter.atNode( |
| variable, |
| WarningCode.INVALID_INTERNAL_ANNOTATION, |
| ); |
| } |
| } |
| } else if (parent is FieldDeclaration) { |
| for (var variable in parent.fields.variables) { |
| var element = variable.declaredElement as FieldElement; |
| if (Identifier.isPrivateName(element.name)) { |
| _errorReporter.atNode( |
| variable, |
| WarningCode.INVALID_INTERNAL_ANNOTATION, |
| ); |
| } |
| } |
| } else if (parent is ConstructorDeclaration) { |
| var class_ = parent.declaredElement!.enclosingElement; |
| if (class_.isPrivate || parentElementIsPrivate) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_INTERNAL_ANNOTATION, |
| ); |
| } |
| } else if (parentElementIsPrivate) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_INTERNAL_ANNOTATION, |
| ); |
| } else if (_inPackagePublicApi) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_INTERNAL_ANNOTATION, |
| ); |
| } |
| } |
| |
| void _checkKinds(Annotation node, AstNode parent, ElementAnnotation element) { |
| var kinds = element.targetKinds; |
| if (kinds.isNotEmpty) { |
| if (!_isValidTarget(parent, kinds)) { |
| var invokedElement = element.element!; |
| var name = invokedElement.name; |
| if (invokedElement is ConstructorElement) { |
| var className = invokedElement.enclosingElement.name; |
| if (name!.isEmpty) { |
| name = className; |
| } else { |
| name = '$className.$name'; |
| } |
| } |
| var kindNames = kinds.map((kind) => kind.displayString).toList() |
| ..sort(); |
| var validKinds = kindNames.commaSeparatedWithOr; |
| // Annotations always refer to named elements, so we can safely assume |
| // that `name` is non-`null`. |
| _errorReporter.atNode( |
| node.name, |
| WarningCode.INVALID_ANNOTATION_TARGET, |
| arguments: [name!, validKinds], |
| ); |
| return; |
| } |
| } |
| } |
| |
| /// Reports a warning if at [node] if it's parent is not a valid target for a |
| /// `@literal` annotation. |
| void _checkLiteral(AstNode node) { |
| var parent = node.parent; |
| if (parent is! ConstructorDeclaration || parent.constKeyword == null) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_LITERAL_ANNOTATION, |
| ); |
| } |
| } |
| |
| /// Reports a warning if [parent] is not a valid target for a |
| /// `@mustBeOverridden` annotation. |
| void _checkMustBeOverridden(Annotation node) { |
| var parent = node.parent; |
| if ((parent is MethodDeclaration && parent.isStatic) || |
| (parent is FieldDeclaration && parent.isStatic) || |
| parent.parent is ExtensionDeclaration || |
| parent.parent is ExtensionTypeDeclaration || |
| parent.parent is EnumDeclaration) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_ANNOTATION_TARGET, |
| arguments: [node.name.name, 'instance members of classes and mixins'], |
| ); |
| } |
| } |
| |
| /// Reports a warning at [node] if it's parent is not a valid target for a |
| /// `@mustCallSuper` annotation. |
| void _checkMustCallSuper(Annotation node) { |
| var parent = node.parent; |
| if ((parent is MethodDeclaration && parent.isStatic) || |
| (parent is FieldDeclaration && parent.isStatic) || |
| parent.parent is ExtensionDeclaration || |
| parent.parent is ExtensionTypeDeclaration || |
| parent.parent is EnumDeclaration) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_ANNOTATION_TARGET, |
| arguments: [node.name.name, 'instance members of classes and mixins'], |
| ); |
| } |
| } |
| |
| /// Reports a warning at [node if it's parent is not a valid target for a |
| /// `@nonVirtual` annotation. |
| void _checkNonVirtual(AstNode node) { |
| var parent = node.parent; |
| if (parent is FieldDeclaration) { |
| if (parent.isStatic) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_NON_VIRTUAL_ANNOTATION, |
| ); |
| } |
| } else if (parent is MethodDeclaration) { |
| if (parent.parent is ExtensionDeclaration || |
| parent.parent is ExtensionTypeDeclaration || |
| parent.isStatic || |
| parent.isAbstract) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_NON_VIRTUAL_ANNOTATION, |
| ); |
| } |
| } else { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_NON_VIRTUAL_ANNOTATION, |
| ); |
| } |
| } |
| |
| /// Reports a warning if [parent] is not a valid target for a |
| /// `@redeclare` annotation. |
| void _checkRedeclare(Annotation node) { |
| var parent = node.parent; |
| if (parent.parent is! ExtensionTypeDeclaration || |
| parent is MethodDeclaration && parent.isStatic) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_ANNOTATION_TARGET, |
| arguments: [node.name.name, 'instance members of extension types'], |
| ); |
| } |
| } |
| |
| /// Reports a warning if [parent] is not a valid target for a `@reopen` |
| /// annotation. |
| void _checkReopen(AstNode node) { |
| ClassElement? classElement; |
| InterfaceElement? superElement; |
| |
| var parent = node.parent; |
| if (parent is ClassDeclaration) { |
| classElement = parent.declaredElement; |
| superElement = classElement?.supertype?.element; |
| } else if (parent is ClassTypeAlias) { |
| classElement = parent.declaredElement; |
| superElement = classElement?.supertype?.element; |
| } else { |
| // If [parent] is neither of the above types, then [_checkKinds] will report |
| // a warning. |
| return; |
| } |
| |
| if (classElement == null) { |
| return; |
| } |
| if (superElement is! ClassElement) { |
| return; |
| } |
| if (classElement.isFinal || |
| classElement.isMixinClass || |
| classElement.isSealed) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_REOPEN_ANNOTATION, |
| ); |
| return; |
| } |
| if (classElement.library != superElement.library) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_REOPEN_ANNOTATION, |
| ); |
| return; |
| } |
| if (classElement.isBase) { |
| if (!superElement.isFinal && !superElement.isInterface) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_REOPEN_ANNOTATION, |
| ); |
| return; |
| } |
| } else if (!classElement.isBase && |
| !classElement.isFinal && |
| !classElement.isInterface && |
| !classElement.isSealed) { |
| if (!superElement.isInterface) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_REOPEN_ANNOTATION, |
| ); |
| return; |
| } |
| } |
| } |
| |
| /// Reports a warning if [parent] is not a valid target for a `@sealed` |
| /// annotation. |
| void _checkSealed(AstNode node) { |
| var parent = node.parent; |
| if (!(parent is ClassDeclaration || parent is ClassTypeAlias)) { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_SEALED_ANNOTATION, |
| ); |
| } |
| } |
| |
| /// Reports a warning if [node], a `@UseResult` annotation, references an |
| /// unknown parameter as an argument to 'unless'. |
| void _checkUseResult(Annotation node, ElementAnnotation element) { |
| var parent = node.parent; |
| var undefinedParameter = |
| _findUndefinedUseResultParameter(element, node, parent); |
| if (undefinedParameter != null) { |
| String? name; |
| if (parent is FunctionDeclaration) { |
| name = parent.name.lexeme; |
| } else if (parent is MethodDeclaration) { |
| name = parent.name.lexeme; |
| } |
| if (name != null) { |
| var parameterName = undefinedParameter is SimpleStringLiteral |
| ? undefinedParameter.value |
| : undefinedParameter.staticParameterElement?.name; |
| _errorReporter.atNode( |
| undefinedParameter, |
| WarningCode.UNDEFINED_REFERENCED_PARAMETER, |
| arguments: [parameterName ?? undefinedParameter, name], |
| ); |
| } |
| } |
| } |
| |
| /// Reports a warning at [node] if it is not a valid target for a |
| /// visibility (`visibleForTemplate`, `visibleOutsideTemplate`, |
| /// `visibleForTesting`, `visibleForOverride`) annotation. |
| void _checkVisibility(Annotation node, ElementAnnotation element) { |
| var parent = node.parent; |
| if (parent is Declaration) { |
| void reportInvalidAnnotation(String name) { |
| // This method is only called on named elements, so it is safe to |
| // assume that `declaredElement.name` is non-`null`. |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_VISIBILITY_ANNOTATION, |
| arguments: [name, node.name.name], |
| ); |
| } |
| |
| void reportInvalidVisibleForOverriding() { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_VISIBLE_FOR_OVERRIDING_ANNOTATION, |
| ); |
| } |
| |
| if (parent is TopLevelVariableDeclaration) { |
| for (VariableDeclaration variable in parent.variables.variables) { |
| var variableElement = |
| variable.declaredElement as TopLevelVariableElement; |
| |
| var variableName = variableElement.name; |
| if (Identifier.isPrivateName(variableName)) { |
| reportInvalidAnnotation(variableName); |
| } |
| |
| if (element.isVisibleForOverriding) { |
| // Top-level variables can't be overridden. |
| reportInvalidVisibleForOverriding(); |
| } |
| } |
| } else if (parent is FieldDeclaration) { |
| for (VariableDeclaration variable in parent.fields.variables) { |
| var fieldElement = variable.declaredElement as FieldElement; |
| if (parent.isStatic && element.isVisibleForOverriding) { |
| reportInvalidVisibleForOverriding(); |
| } |
| |
| var fieldName = fieldElement.name; |
| if (Identifier.isPrivateName(fieldName)) { |
| reportInvalidAnnotation(fieldName); |
| } |
| } |
| } else if (parent.declaredElement != null) { |
| var declaredElement = parent.declaredElement!; |
| if (element.isVisibleForOverriding && |
| (!declaredElement.isInstanceMember || |
| declaredElement.enclosingElement is ExtensionTypeElement)) { |
| reportInvalidVisibleForOverriding(); |
| } |
| |
| var name = declaredElement.name; |
| if (name != null && Identifier.isPrivateName(name)) { |
| reportInvalidAnnotation(name); |
| } |
| } |
| } else { |
| // Something other than a declaration was annotated. Whatever this is, |
| // it probably warrants a Warning, but this has not been specified on |
| // `visibleForTemplate` or `visibleForTesting`, so leave it alone for |
| // now. |
| } |
| } |
| |
| /// Reports a warning at [node] if it's parent is not a valid target for an |
| /// `@visibleOutsideTemplate` annotation. |
| void _checkVisibleOutsideTemplate(Annotation node) { |
| void reportError() { |
| _errorReporter.atNode( |
| node, |
| WarningCode.INVALID_VISIBLE_OUTSIDE_TEMPLATE_ANNOTATION, |
| ); |
| } |
| |
| AstNode? containedDeclaration; |
| switch (node.parent) { |
| case ConstructorDeclaration constructorDeclaration: |
| containedDeclaration = constructorDeclaration; |
| case EnumConstantDeclaration enumConstant: |
| containedDeclaration = enumConstant; |
| case FieldDeclaration fieldDeclaration: |
| containedDeclaration = fieldDeclaration; |
| case MethodDeclaration methodDeclaration: |
| containedDeclaration = methodDeclaration; |
| default: |
| reportError(); |
| return; |
| } |
| |
| InterfaceElement? declaredElement; |
| switch (containedDeclaration.parent) { |
| case ClassDeclaration classDeclaration: |
| declaredElement = classDeclaration.declaredElement; |
| case EnumDeclaration enumDeclaration: |
| declaredElement = enumDeclaration.declaredElement; |
| case MixinDeclaration mixinDeclaration: |
| declaredElement = mixinDeclaration.declaredElement; |
| default: |
| reportError(); |
| return; |
| } |
| |
| if (declaredElement == null) { |
| reportError(); |
| return; |
| } |
| |
| for (var annotation in declaredElement.metadata) { |
| if (annotation.isVisibleForTemplate) { |
| return; |
| } |
| } |
| |
| reportError(); |
| } |
| |
| /// Returns an expression (for error-reporting purposes) associated with a |
| /// `@useResult` `unless` argument, if the associated parameter is undefined. |
| Expression? _findUndefinedUseResultParameter( |
| ElementAnnotation element, Annotation node, AstNode parent) { |
| var constructorName = node.name; |
| if (constructorName is! PrefixedIdentifier || |
| constructorName.identifier.name != 'unless') { |
| return null; |
| } |
| |
| var unlessParam = element |
| .computeConstantValue() |
| ?.getField('parameterDefined') |
| ?.toStringValue(); |
| if (unlessParam == null) { |
| return null; |
| } |
| |
| Expression? checkParams(FormalParameterList? parameterList) { |
| if (parameterList == null) { |
| return null; |
| } |
| |
| for (var parameter in parameterList.parameters) { |
| if (parameter.name?.lexeme == unlessParam) { |
| return null; |
| } |
| } |
| |
| // Find and return the parameter value node. |
| var arguments = node.arguments?.arguments; |
| if (arguments == null) { |
| return null; |
| } |
| |
| for (var arg in arguments) { |
| if (arg is NamedExpression && |
| arg.name.label.name == 'parameterDefined') { |
| return arg.expression; |
| } |
| } |
| |
| return null; |
| } |
| |
| if (parent is FunctionDeclarationImpl) { |
| return checkParams(parent.functionExpression.parameters); |
| } |
| if (parent is MethodDeclarationImpl) { |
| return checkParams(parent.parameters); |
| } |
| |
| return null; |
| } |
| |
| /// Returns whether it is valid to have an annotation on the given [target] |
| /// when the annotation is marked as being valid for the given [kinds] of |
| /// targets. |
| bool _isValidTarget(AstNode target, Set<TargetKind> kinds) { |
| // `TargetKind.overridableMember` is complex, so we handle it separately. |
| if (kinds.contains(TargetKind.overridableMember)) { |
| if ((target is FieldDeclaration && !target.isStatic) || |
| target is MethodDeclaration && !target.isStatic) { |
| var parent = target.parent; |
| if (parent is ClassDeclaration || |
| parent is ExtensionTypeDeclaration || |
| parent is MixinDeclaration) { |
| // Members of `EnumDeclaration`s and `ExtensionDeclaration`s are not |
| // overridable. |
| return true; |
| } |
| } |
| } |
| |
| return switch (target) { |
| ClassDeclaration() => |
| kinds.contains(TargetKind.classType) || kinds.contains(TargetKind.type), |
| ClassTypeAlias() => |
| kinds.contains(TargetKind.classType) || kinds.contains(TargetKind.type), |
| ConstructorDeclaration() => kinds.contains(TargetKind.constructor), |
| Directive() => kinds.contains(TargetKind.directive) || |
| (target.parent as CompilationUnit).directives.first == target && |
| kinds.contains(TargetKind.library), |
| EnumConstantDeclaration() => kinds.contains(TargetKind.enumValue), |
| EnumDeclaration() => |
| kinds.contains(TargetKind.enumType) || kinds.contains(TargetKind.type), |
| ExtensionTypeDeclaration() => kinds.contains(TargetKind.extensionType), |
| ExtensionDeclaration() => kinds.contains(TargetKind.extension), |
| FieldDeclaration() => kinds.contains(TargetKind.field), |
| FunctionDeclaration(isGetter: true) => kinds.contains(TargetKind.getter), |
| FunctionDeclaration(isSetter: true) => kinds.contains(TargetKind.setter), |
| FunctionDeclaration() => kinds.contains(TargetKind.function), |
| MethodDeclaration(isGetter: true) => kinds.contains(TargetKind.getter), |
| MethodDeclaration(isSetter: true) => kinds.contains(TargetKind.setter), |
| MethodDeclaration() => kinds.contains(TargetKind.method), |
| MixinDeclaration() => |
| kinds.contains(TargetKind.mixinType) || kinds.contains(TargetKind.type), |
| FormalParameter() => kinds.contains(TargetKind.parameter) || |
| (target.isOptional && kinds.contains(TargetKind.optionalParameter)), |
| FunctionTypeAlias() || |
| GenericTypeAlias() => |
| kinds.contains(TargetKind.typedefType) || |
| kinds.contains(TargetKind.type), |
| TopLevelVariableDeclaration() => |
| kinds.contains(TargetKind.topLevelVariable), |
| TypeParameter() => kinds.contains(TargetKind.typeParameter), |
| _ => false, |
| }; |
| } |
| } |