Skip to content

How will annotations on a class with a primary constructor apply? #62253

@srawlins

Description

@srawlins

(Note the sibiling issue, #62254, for instance-variable-declaring parameters.)

With primary constructors, you can write a class like so:

@visibleForTesting
class C(final int x);

The constructor and class declarations are rather intertwined, and there is no way to annotate the constructor individually (you cannot write class C @visibleForTesting (final int x);). So given the above declaration, does @visibleForTesting apply to the class? The constructor? Both?

One option is to say: that annotation only applies to the class; if you want any annotation on the constructor, you cannot write this class with a primary constructor. That seems pretty harsh, and may block a significant set of potential primary constructor use cases.

A second option is to say: each tool (e.g. analyzer, freezed code generator, JsonSerializable code generator) must choose a sensible option, based on the annotation. (And the choice should be documented with the annotation.) I'd like to explore this option. Regarding the analyzer's static analysis, It only works for annotations that the analyzer understands and acts on, which includes annotations from:

  • dart:core (@Deprecated(),@override, @Since()),
  • package:meta (@immutable, @mustCallSuper, @visibleForTesting, etc.),
  • a few other spots, like I think dart:ffi, dart:js_interop (@Js), package:test_reflective_loader,
  • @Preview from somewhere in Flutter,
  • possibly specific cases of @pragma.
  • Maybe more. The analyzer does not act on external things like a @freezed annotation (except maybe to report when it is applied wrongly, if it uses @Target; we'll get to that below).

(I'll note that "each tool must choose a sensible option" is not really drifting from how things work today. When you annotate a class with @visibleForTesting, we choose whether or not that applies to members, like constructors. When you annotate a method with @protected, we choose whether or not that applies to overrides.)

So, what might this look like? Let's rundown package:meta:

Annotation is considered to apply to
@awaitNotRequired n/a (Future-returning function)
@doNotStore The constructor
@doNotSubmit Both
@experimental Both
@factory n/a (static method)
@immutable The class
@internal Both
@isTest n/a (non-constructor function)
@isTestGroup n/a (non-constructor function)
@literal The constructor
@mustBeConst n/a (parameter)
@mustBeOverridden n/a (instance member)
@mustCallSuper n/a (instance member)
@nonVirtual n/a (instance member)
@optionalTypeArgs The class
@protected n/a (instance member)
@RecordUse Maybe the constructor
@redeclare n/a (instance member)
@reopen The class
@Target The class
@sealed The class
@useResult The constructor, or maybe n/a
@visibleForOverriding n/a (instance member)
@visibleForTesting Both

In all of these cases, "The class" is shorthand for "The class, enum, extension type (or extension constructor?)" where applicable.

How about dart:core:

Annotation is considered to apply to
@Deprecated Both
@override n/a (instance member)
@Since Both
@pragma Does the analyzer act on any pragma annotations?

If this seems a sensible route, we can move forward and define what annotations from dart:ffi etc. apply to.

TargetKind

TargetKind offers a neat way for the analyzer to report on whether an arbitrary annotation has been applied to a sensible element. For example, the @immutable annotation is itself annotated with @Target({.classType, .extensionType, .mixinType}) which indicates that it is only appropriate to use this annotation on a class, extension type, or mixin declaration. If it is found on any other element, like a variable, the analyzer will report a warning.

That set of TargetKinds for @immutable serves as a good example: if a class with a primary constructor is annotated with @immutable, is that "ok?" It seems obviously "yes" in this case, we can take a wild guess at what the user is intending. And we don't have to complain that it looks like the annotation might apply to the constructor. Indeed, classes with a bunch of final fields declared in a primary constructor is a prime target for an @immutable annotation. So what about the other TargetKinds?

  • classType, enumType, extensionType, mixinType, type are appropriate, as the annotation can be considered to apply to the type declaration.
  • constructor is appropriate, as the annotation can be considered to apply to the constructor declaration.
  • directive, enumValue, extension, field, function, getter, library, method, optionalParameter, overridableMember, parameter, setter, topLevelVariable, typedefType, typeParameter, values are not appropriate; continue to report.

CC @bwilkerson @szakarias @eernstg @pq

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2A bug or feature request we're likely to work onarea-devexpFor issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages.devexp-linterIssues with the analyzer's support for the linter packagedevexp-warningIssues with the analyzer's Warning codesfeature-primary-constructorsImplementation of the primary constructors feature. Otherwise known as declaring constructors.type-enhancementA request for a change that isn't a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions