Add ElementAnnotation.isValidAtElement()

Bug: https://github.com/dart-lang/sdk/issues/63503
Change-Id: Iadc096c244607edcb773d70338f1b5b769c25128
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/509560
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
This commit is contained in:
Konstantin Shcheglov
2026-06-05 18:02:28 -07:00
parent 9a769de4b7
commit 778a860a2a
9 changed files with 298 additions and 126 deletions
@@ -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;
}
+1
View File
@@ -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())
@@ -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.
///
@@ -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<TargetKind> _overrideTargetKinds = {
TargetKind.field,
TargetKind.getter,
TargetKind.method,
TargetKind.setter,
};
final Map<String, TargetKind> _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<TargetKind> 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<TargetKind>? 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;
}
}
@@ -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();
@@ -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<String, TargetKind> _targetKindsByName = {
for (var kind in TargetKind.values) kind.name: kind,
};
/// Return the target kinds defined for this [ElementAnnotation].
Set<TargetKind> 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 <TargetKind>{};
}
for (var annotation in interfaceElement.metadata.annotations) {
if (annotation.isTarget) {
var value = annotation.computeConstantValue();
if (value == null) {
return const <TargetKind>{};
}
var annotationKinds = value.getField('kinds')?.toSetValue();
if (annotationKinds == null) {
return const <TargetKind>{};
}
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 <TargetKind>{};
}
}
extension ExecutableElement2Extension on ExecutableElement {
/// Whether the enclosing element is the class `Object`.
bool get isObjectMember {
@@ -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<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;
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<Element> _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 {
@@ -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
@@ -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<void> {
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) {