diff --git a/pkg/analysis_server/lib/src/services/correction/organize_imports.dart b/pkg/analysis_server/lib/src/services/correction/organize_imports.dart index 65b08c854b0..db832edd938 100644 --- a/pkg/analysis_server/lib/src/services/correction/organize_imports.dart +++ b/pkg/analysis_server/lib/src/services/correction/organize_imports.dart @@ -10,7 +10,7 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:analyzer/src/dart/ast/ast.dart'; -import 'package:analyzer/src/dart/element/extensions.dart'; +import 'package:analyzer/src/dart/element/annotation_target.dart'; import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; import 'package:analyzer/src/ignore_comments/ignore_info.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart' @@ -358,7 +358,7 @@ class ImportOrganizer { IgnoreInfo.isIgnoreComment(token.lexeme); static bool _isLibraryTargetAnnotation(Annotation annotation) => - annotation.elementAnnotation?.targetKinds.contains(TargetKind.library) ?? + annotation.elementAnnotation?.targetKinds?.contains(TargetKind.library) ?? false; } diff --git a/pkg/analyzer/api.txt b/pkg/analyzer/api.txt index da76367103d..ebbf9a3099e 100644 --- a/pkg/analyzer/api.txt +++ b/pkg/analyzer/api.txt @@ -3501,6 +3501,7 @@ package:analyzer/dart/element/element.dart: isWidgetFactory (getter: bool) libraryFragment (getter: LibraryFragment) computeConstantValue (method: DartObject? Function()) + isValidAtElement (method: bool? Function(Element)) toSource (method: String Function()) ElementDirective (class extends Object, abstract): new (constructor: ElementDirective Function()) diff --git a/pkg/analyzer/lib/dart/element/element.dart b/pkg/analyzer/lib/dart/element/element.dart index 8e34f014967..ceceef5c61e 100644 --- a/pkg/analyzer/lib/dart/element/element.dart +++ b/pkg/analyzer/lib/dart/element/element.dart @@ -766,6 +766,16 @@ abstract class ElementAnnotation { /// if the value of this annotation could not be computed because of errors. DartObject? computeConstantValue(); + /// Whether this annotation is a valid annotation for the given [element]. + /// + /// Returns `true` if the annotation has known target kinds, and the + /// [element] matches one of them. Returns `false` if the annotation has known + /// target kinds, and the [element] does not match any of them. + /// + /// Returns `null` if there is no known set of target kinds for this + /// annotation. + bool? isValidAtElement(Element element); + /// Returns a textual description of this annotation in a form approximating /// valid source. /// diff --git a/pkg/analyzer/lib/src/dart/element/annotation_target.dart b/pkg/analyzer/lib/src/dart/element/annotation_target.dart new file mode 100644 index 00000000000..fad8ace3d68 --- /dev/null +++ b/pkg/analyzer/lib/src/dart/element/annotation_target.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2026, 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:meta/meta_meta.dart'; + +const Set _overrideTargetKinds = { + TargetKind.field, + TargetKind.getter, + TargetKind.method, + TargetKind.setter, +}; + +final Map _targetKindsByName = { + for (var kind in TargetKind.values) kind.name: kind, +}; + +bool? isAnnotationValidAtElement( + ElementAnnotation annotation, + Element element, +) { + var kinds = annotation.targetKinds; + if (kinds == null) { + return null; + } + return isValidAnnotationTargetElement(element, kinds); +} + +bool isValidAnnotationTargetElement(Element element, Set kinds) { + if (kinds.contains(TargetKind.overridableMember) && + _isOverridableMember(element)) { + return true; + } + + return switch (element) { + ClassElement() => + kinds.contains(TargetKind.classType) || kinds.contains(TargetKind.type), + ConstructorElement() => kinds.contains(TargetKind.constructor), + EnumElement() => + kinds.contains(TargetKind.enumType) || kinds.contains(TargetKind.type), + ExtensionElement() => kinds.contains(TargetKind.extension), + ExtensionTypeElement() => kinds.contains(TargetKind.extensionType), + FieldElement(:var isEnumConstant) => + isEnumConstant + ? kinds.contains(TargetKind.enumValue) + : kinds.contains(TargetKind.field), + FormalParameterElement(:var isOptional) => + kinds.contains(TargetKind.parameter) || + isOptional && kinds.contains(TargetKind.optionalParameter), + GetterElement() => kinds.contains(TargetKind.getter), + LibraryElement() => kinds.contains(TargetKind.library), + LocalFunctionElement() || + TopLevelFunctionElement() => kinds.contains(TargetKind.function), + MethodElement() => kinds.contains(TargetKind.method), + MixinElement() => + kinds.contains(TargetKind.mixinType) || kinds.contains(TargetKind.type), + SetterElement() => kinds.contains(TargetKind.setter), + TopLevelVariableElement() => kinds.contains(TargetKind.topLevelVariable), + TypeAliasElement() => + kinds.contains(TargetKind.typedefType) || kinds.contains(TargetKind.type), + TypeParameterElement() => kinds.contains(TargetKind.typeParameter), + _ => false, + }; +} + +bool _isOverridableMember(Element element) { + if (element case FieldElement(:var isStatic, :var enclosingElement)) { + return !isStatic && _isOverridableMemberContainer(enclosingElement); + } + + if (element case GetterElement(:var isStatic, :var enclosingElement)) { + return !isStatic && _isOverridableMemberContainer(enclosingElement); + } + + if (element case MethodElement(:var isStatic, :var enclosingElement)) { + return !isStatic && _isOverridableMemberContainer(enclosingElement); + } + + if (element case SetterElement(:var isStatic, :var enclosingElement)) { + return !isStatic && _isOverridableMemberContainer(enclosingElement); + } + + return false; +} + +bool _isOverridableMemberContainer(Element? element) { + return element is ClassElement || + element is ExtensionTypeElement || + element is MixinElement; +} + +extension ElementAnnotationTargetKinds on ElementAnnotation { + /// Return the known target kinds for this annotation. + /// + /// Returns `null` if there is no known set of target kinds. + Set? get targetKinds { + if (isOverride) { + return _overrideTargetKinds; + } + + var element = this.element; + InterfaceElement? interfaceElement; + if (element is GetterElement) { + var type = element.returnType; + if (type is InterfaceType) { + interfaceElement = type.element; + } + } else if (element is ConstructorElement) { + interfaceElement = element.enclosingElement; + } + + if (interfaceElement == null) { + return null; + } + + for (var annotation in interfaceElement.metadata.annotations) { + if (annotation.isTarget) { + var value = annotation.computeConstantValue(); + if (value == null) { + return null; + } + + var annotationKinds = value.getField('kinds')?.toSetValue(); + if (annotationKinds == null) { + return null; + } + + return annotationKinds + .map((e) { + // Support class-based and enum-based target kind implementations. + var field = e.getField('name') ?? e.getField('_name'); + return field?.toStringValue(); + }) + .map((name) => _targetKindsByName[name]) + .nonNulls + .toSet(); + } + } + + return null; + } +} diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart index 6f92dd3bf78..98a7b09ff20 100644 --- a/pkg/analyzer/lib/src/dart/element/element.dart +++ b/pkg/analyzer/lib/src/dart/element/element.dart @@ -30,6 +30,7 @@ import 'package:analyzer/src/dart/ast/token.dart'; import 'package:analyzer/src/dart/constant/compute.dart'; import 'package:analyzer/src/dart/constant/evaluation.dart'; import 'package:analyzer/src/dart/constant/value.dart'; +import 'package:analyzer/src/dart/element/annotation_target.dart'; import 'package:analyzer/src/dart/element/class_hierarchy.dart'; import 'package:analyzer/src/dart/element/display_string_builder.dart'; import 'package:analyzer/src/dart/element/field_name_non_promotability_info.dart'; @@ -1954,6 +1955,11 @@ class ElementAnnotationImpl return null; } + @override + bool? isValidAtElement(Element element) { + return isAnnotationValidAtElement(this, element); + } + @override String toSource() => annotationAst.toSource(); diff --git a/pkg/analyzer/lib/src/dart/element/extensions.dart b/pkg/analyzer/lib/src/dart/element/extensions.dart index 7a13a1e65a3..17b94199432 100644 --- a/pkg/analyzer/lib/src/dart/element/extensions.dart +++ b/pkg/analyzer/lib/src/dart/element/extensions.dart @@ -13,7 +13,6 @@ import 'package:analyzer/src/dart/element/type.dart'; import 'package:analyzer/src/diagnostic/diagnostic.dart' show DiagnosticMessageImpl; import 'package:analyzer/src/generated/utilities_dart.dart'; -import 'package:meta/meta_meta.dart'; extension DartTypeExtension on DartType { bool get isExtensionType { @@ -115,54 +114,6 @@ extension Element2Extension on Element { } } -extension ElementAnnotationExtension on ElementAnnotation { - static final Map _targetKindsByName = { - for (var kind in TargetKind.values) kind.name: kind, - }; - - /// Return the target kinds defined for this [ElementAnnotation]. - Set get targetKinds { - var element = this.element; - InterfaceElement? interfaceElement; - - if (element is GetterElement) { - var type = element.returnType; - if (type is InterfaceType) { - interfaceElement = type.element; - } - } else if (element is ConstructorElement) { - interfaceElement = element.enclosingElement; - } - if (interfaceElement == null) { - return const {}; - } - for (var annotation in interfaceElement.metadata.annotations) { - if (annotation.isTarget) { - var value = annotation.computeConstantValue(); - if (value == null) { - return const {}; - } - - var annotationKinds = value.getField('kinds')?.toSetValue(); - if (annotationKinds == null) { - return const {}; - } - - return annotationKinds - .map((e) { - // Support class-based and enum-based target kind implementations. - var field = e.getField('name') ?? e.getField('_name'); - return field?.toStringValue(); - }) - .map((name) => _targetKindsByName[name]) - .nonNulls - .toSet(); - } - } - return const {}; - } -} - extension ExecutableElement2Extension on ExecutableElement { /// Whether the enclosing element is the class `Object`. bool get isObjectMember { diff --git a/pkg/analyzer/lib/src/error/annotation_verifier.dart b/pkg/analyzer/lib/src/error/annotation_verifier.dart index c398dd5c841..82f7bd7db67 100644 --- a/pkg/analyzer/lib/src/error/annotation_verifier.dart +++ b/pkg/analyzer/lib/src/error/annotation_verifier.dart @@ -5,6 +5,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/dart/ast/ast.dart'; +import 'package:analyzer/src/dart/element/annotation_target.dart'; import 'package:analyzer/src/dart/element/extensions.dart'; import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; import 'package:analyzer/src/error/listener.dart'; @@ -373,18 +374,8 @@ class AnnotationVerifier { } void _checkKinds(Annotation node, AstNode parent, ElementAnnotation element) { - // As `@override` is declared in the Dart SDK, `TargetKind` is unavailable - // to it. - var kinds = element.isOverride - ? { - TargetKind.field, - TargetKind.getter, - TargetKind.method, - TargetKind.setter, - } - : element.targetKinds; - - if (kinds.isNotEmpty) { + var kinds = element.targetKinds; + if (kinds != null && kinds.isNotEmpty) { if (!_isValidTarget(parent, kinds)) { var invokedElement = element.element!; var name = invokedElement.name; @@ -720,36 +711,6 @@ class AnnotationVerifier { /// when the annotation is marked as being valid for the given [kinds] of /// targets. bool _isValidTarget(AstNode target, Set 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; - var parent2 = parent?.parent; - if (parent is BlockClassBody && - (parent2 is ClassDeclaration || - parent2 is ExtensionTypeDeclaration || - parent2 is MixinDeclaration)) { - // Members of `EnumDeclaration`s and `ExtensionDeclaration`s are not - // overridable. - return true; - } - } - if (target is FormalParameter) { - var element = target.declaredFragment?.element; - if (element is FieldFormalParameterElement && element.isDeclaring) { - return true; - } - } - } - - if (target is FormalParameter) { - var element = target.declaredFragment?.element; - if (element is FieldFormalParameterElement && element.isDeclaring) { - if (kinds.contains(TargetKind.field)) return true; - } - } - // Handle the case of the deprecated `TargetKind.directive` before handling // the Directive subclasses below. // ignore: deprecated_member_use @@ -767,43 +728,52 @@ class AnnotationVerifier { } } + for (var element in _targetElements(target)) { + if (isValidAnnotationTargetElement(element, kinds)) { + 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), - EnumConstantDeclaration() => kinds.contains(TargetKind.enumValue), - EnumDeclaration() => - kinds.contains(TargetKind.enumType) || kinds.contains(TargetKind.type), ExportDirective() => kinds.contains(TargetKind.exportDirective), - 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), ImportDirective() => kinds.contains(TargetKind.importDirective), - 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), PartOfDirective() => kinds.contains(TargetKind.partOfDirective), - PrimaryConstructorBody() => kinds.contains(TargetKind.constructor), - 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, }; } + + /// Maps the syntax where an annotation is written to the element targets it + /// can be understood to annotate. + /// + /// Usually this is a single declared element, but some syntax, such as a + /// field declaration with multiple variables or a declaring formal parameter, + /// can correspond to multiple element targets. + List _targetElements(AstNode target) { + switch (target) { + case FieldDeclaration(:var fields): + return [ + for (var variable in fields.variables) + ?variable.declaredFragment?.element, + ]; + case FormalParameter(): + var element = target.declaredFragment?.element; + if (element is FieldFormalParameterElement && element.isDeclaring) { + return [element, ?element.field]; + } + return [?element]; + case PrimaryConstructorBody(:var declaration): + return [?declaration?.declaredFragment?.element]; + case TopLevelVariableDeclaration(:var variables): + return [ + for (var variable in variables.variables) + ?variable.declaredFragment?.element, + ]; + case Declaration(:var declaredFragment): + return [?declaredFragment?.element]; + default: + return const []; + } + } } extension on ClassElement { diff --git a/pkg/analyzer/test/src/dart/element/element_test.dart b/pkg/analyzer/test/src/dart/element/element_test.dart index 83d7dd3c927..34ad25cb013 100644 --- a/pkg/analyzer/test/src/dart/element/element_test.dart +++ b/pkg/analyzer/test/src/dart/element/element_test.dart @@ -27,6 +27,12 @@ main() { @reflectiveTest class ElementAnnotationImplTest extends PubPackageResolutionTest { + @override + void setUp() { + super.setUp(); + writeTestPackageConfigWithMeta(); + } + test_computeConstantValue() async { newFile('$testPackageLibPath/a.dart', r''' class A { @@ -49,6 +55,88 @@ main() { DartObject value = annotation.computeConstantValue()!; expect(value.getField('f')!.toStringValue(), 'x'); } + + test_isValidAtElement_declaringFormalParameter() async { + var result = await resolveTestCodeWithDiagnostics(r''' +import 'package:meta/meta_meta.dart'; + +@Target({TargetKind.field}) +class FieldOnly { + const FieldOnly(); +} + +@Target({TargetKind.parameter}) +class ParameterOnly { + const ParameterOnly(); +} + +class A { + @FieldOnly() + final int explicit = 0; +} + +class B(@FieldOnly() @ParameterOnly() final int f); +'''); + + var explicitField = result.findElement.field('explicit', of: 'A'); + var declaringField = result.findElement.field('f', of: 'B'); + var declaringParameter = result.findElement + .class_('B') + .constructors + .single + .formalParameters + .single; + + var explicitFieldOnly = explicitField.metadata.annotations.single; + expect(explicitFieldOnly.isValidAtElement(explicitField), isTrue); + expect(explicitFieldOnly.isValidAtElement(declaringParameter), isFalse); + + var declaringFieldOnly = declaringParameter.metadata.annotations[0]; + var declaringParameterOnly = declaringParameter.metadata.annotations[1]; + expect(declaringFieldOnly.isValidAtElement(declaringParameter), isFalse); + expect(declaringFieldOnly.isValidAtElement(declaringField), isTrue); + expect(declaringParameterOnly.isValidAtElement(declaringParameter), isTrue); + expect(declaringParameterOnly.isValidAtElement(declaringField), isFalse); + } + + test_isValidAtElement_noTarget() async { + var result = await resolveTestCodeWithDiagnostics(r''' +class A { + const A(); +} + +@A() +class B {} +'''); + var class_ = result.findElement.class_('B'); + var annotation = class_.metadata.annotations.single; + + expect(annotation.isValidAtElement(class_), isNull); + } + + test_isValidAtElement_override() async { + var result = await resolveTestCodeWithDiagnostics(r''' +class A { + void m() {} +} + +class B extends A { + @override + void m() {} + + void n(int p) {} +} +'''); + var method = result.findElement.method('m', of: 'B'); + var parameter = result.findElement + .method('n', of: 'B') + .formalParameters + .single; + var annotation = method.metadata.annotations.single; + + expect(annotation.isValidAtElement(method), isTrue); + expect(annotation.isValidAtElement(parameter), isFalse); + } } @reflectiveTest diff --git a/pkg/linter/lib/src/rules/library_annotations.dart b/pkg/linter/lib/src/rules/library_annotations.dart index 55a716c8153..29a7ba64775 100644 --- a/pkg/linter/lib/src/rules/library_annotations.dart +++ b/pkg/linter/lib/src/rules/library_annotations.dart @@ -10,7 +10,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/error/error.dart'; // ignore: implementation_imports -import 'package:analyzer/src/dart/element/extensions.dart'; +import 'package:analyzer/src/dart/element/annotation_target.dart'; import 'package:meta/meta_meta.dart'; import '../analyzer.dart'; @@ -64,8 +64,10 @@ class _Visitor extends SimpleAstVisitor { return; } - if (elementAnnotation.targetKinds.length == 1 && - elementAnnotation.targetKinds.contains(TargetKind.library) && + var targetKinds = elementAnnotation.targetKinds; + if (targetKinds != null && + targetKinds.length == 1 && + targetKinds.contains(TargetKind.library) && firstDirective == node) { rule.reportAtNode(annotation); } else if (elementAnnotation.isPragmaLateTrust) {