Augmentations

July 20, 2026 ยท View on GitHub

Authors: rnystrom@google.com, jakemac@google.com, lrn@google.com, eernst@google.com

Version: 1.44 (see Changelog at end)

Experiment flag: augmentations

Augmentations allow splitting a declaration across multiple locations, both within a single file and across multiple files. They can add new top-level declarations, inject new members into classes, and provide bodies for functions.

Motivation

Dart libraries are the unit of code reuse. When an API is too large to fit into a single file, you can usually split it into multiple libraries and then have one main library export the others. That works well when the functionality in each file is made of separate top-level declarations.

However, sometimes a single declaration is too large to fit comfortably in a file. Dart libraries and even part files are no help there. Because of this, users have asked for something like partial classes in C# (#252 83 ๐Ÿ‘, #678 20 ๐Ÿ‘). C# also supports splitting the declaration and implementation of methods into separate files.

Generated code

Size isn't the only reason to split a library into multiple files. Code generation is common in Dart. AngularDart compiles HTML templates to Dart files. The freezed and built_value packages generate Dart code to implement immutable data structures.

In cases like this, it's important to have the hand-authored and machine-generated code in separate files so that the code generator doesn't inadvertently erase a user's code. AngularDart generates a separate library for the component. The freezed and built_value packages generate part files.

Note that the relationship between the hand-authored and generated code can go in both directions:

  • Often, a user hand-authors a skeleton declaration and then a code generator fills in implementation and adds capabilities to it. That's how freezed and built_value work.

  • Other times, a code generator produces code with default basic behavior that a human author then wants to tweak or refine. You see this sometimes with FFI where a code generator provides a default API to some external system but where you want to layer on hand-authored code to provide a more natural Dart-like experience.

Having a mixture of hand-authored and generated code works well when the generated code consists of completely separate declarations from the hand-authored code. But if a code generator wants to, say, add a method to a hand-authored class, or an author wants to add a method to a generated class, then the language is of little help. This proposal addresses that limitation by adding augmentations.

Augmentation declarations

A declaration marked with the modifier augment creates an augmentation declaration (or just "augmentation"). In contrast, an unmarked declaration is an introductory declaration.

An augmentation declaration doesn't create a new entity in the program. Instead, it adds to some existing introductory declaration with the same name. Augmentation declarations can affect types, functions, members, and almost any other kind of declaration in Dart. They can add members to classes, append values to enums, append types to with or implements clauses, fill in missing function bodies, etc.

An augmentation can be in the same file as the introductory declaration it applies to, or in separate part files, but they must both be in the same library. Augmentations allow authoring a library in multiple separate pieces or files (some of which are likely generated) in a flexible manner, but they can't inject new behavior into libraries outside of the library author's control.

Design principle

When designing this feature, a fundamental question is how much power to give augmentations. Giving them more ability to change the introductory declaration makes them more powerful and expressive. But the more an augmentation can change, the less a reader can correctly assume from reading only the introductory declaration. That can make code using augmentations harder to understand and work with.

To balance those, the general principle of this feature is that augmentations can add new capabilities to the declaration and fill in implementation, but generally can't change any property a reader knows to be true from the introductory declaration or any prior augmentation. In other words, if a program would work without the augmentation being applied, it should generally still work after the augmentation is applied. Note that this a design principle and not a strict guarantee.

For example, if the introductory declaration of a function takes an int parameter and returns a String, then any augmentation must also take an int and return a String. That way a reader knows how to call the function and what they'll get back without having to read the augmentations.

Likewise, if an introductory class declaration has a generative constructor, then the reader assumes they can inherit from that class and call that as a superclass constructor. Therefore, an augmentation of the class is prohibited from changing the constructor to a factory.

Syntax

The syntax changes are simple but fairly extensive and touch several parts of the grammar so are broken out into separate sections.

Top-level augmentations and incomplete top-level members

We allow an augment modifier before most top-level declarations.

Also, we allow incomplete declarations at the top level. This reuses the same syntax used inside a class to declare abstract variables, methods, getters, setters, and operators. For callable members, that means the body is ;. For variable declarations, that means using abstract. Example:

abstract int x;   // Incomplete top-level variable.
int get y;        // Incomplete top-level getter.
set z(int value); // Incomplete top-level setter.

The new top-level grammar is:

topLevelDeclaration ::= classDeclaration
  | mixinDeclaration
  | extensionTypeDeclaration
  | extensionDeclaration
  | enumType
  | typeAlias
  | 'augment'? 'external' functionSignature ';'
  | 'augment'? 'external' getterSignature ';'
  | 'augment'? 'external' setterSignature ';'
  | 'augment'? 'external' finalVarOrType identifierList ';'
  | 'augment'? 'abstract' finalVarOrType identifierList ';'
  | 'augment'? getterSignature (functionBody | ';')
  | 'augment'? setterSignature (functionBody | ';')
  | 'augment'? functionSignature (functionBody | ';')
  | 'augment'? ('final' | 'const') type? staticFinalDeclarationList ';'
  | 'augment'? 'late' 'final' type? initializedIdentifierList ';'
  | 'augment'? 'late'? varOrType initializedIdentifierList ';'

Class-like declarations

We allow augment before class, extension type, and mixin declarations. (Enums and extensions are discussed in subsequent sections.)

classDeclaration ::=
    'augment'? (classModifiers | mixinClassModifiers)
    'class' classNameMaybePrimary superclass? interfaces?
    memberedDeclarationBody
  | classModifiers 'mixin'? 'class' mixinApplicationClass

primaryConstructor ::= // From primary constructors specification.
     'const'? typeWithParameters ('.' identifierOrNew)?
     declaringParameterList;

classNameMaybePrimary ::= // From primary constructors specification.
     primaryConstructor
   | typeWithParameters;

mixinDeclaration ::=
    'base'? 'mixin' typeIdentifier typeParameters?
    ('on' typeNotVoidNotFunctionList)? interfaces? memberedDeclarationBody
  | 'augment' 'base'? 'mixin' typeIdentifier typeParameters?
    interfaces? memberedDeclarationBody

extensionTypeDeclaration ::= // From primary constructors specification
    'extension' 'type' primaryConstructor interfaces?
    memberedDeclarationBody
  | 'augment' 'extension' 'type' typeIdentifier
    typeParameters? interfaces?
    memberedDeclarationBody

memberedDeclarationBody ::=
    '{' memberDeclarations '}'
  | ';'

memberDeclarations ::= (metadata 'augment'? memberDeclaration)*

primaryConstructorBodySignature ::= // From primary constructors specification
     'augment'? 'this' initializers?

memberDeclaration ::= declaration
  | methodSignature functionBody

declaration ::=
    'external'? factoryConstructorSignature ';'
  | 'external' constantConstructorSignature ';'
  | 'external' constructorSignature ';'
  | 'external'? 'static'? getterSignature ';'
  | 'external'? 'static'? setterSignature ';'
  | 'external'? 'static'? functionSignature ';'
  | 'external'? operatorSignature ';'
  | 'external' ('static'? finalVarOrType | 'covariant' varOrType)
    identifierList ';'
  | 'abstract' (finalVarOrType | 'covariant' varOrType) identifierList ';'
  | 'static' 'abstract' finalVarOrType identifierList ';'
  | 'static' 'const' type? staticFinalDeclarationList ';'
  | 'static' 'final' type? staticFinalDeclarationList ';'
  | 'static' 'late' 'final' type? initializedIdentifierList ';'
  | 'static' 'late'? varOrType initializedIdentifierList ';'
  | 'covariant' 'late' 'final' type? identifierList ';'
  | 'covariant' 'late'? varOrType initializedIdentifierList ';'
  | 'late'? 'final' type? initializedIdentifierList ';'
  | 'late'? varOrType initializedIdentifierList ';'
  | redirectingFactoryConstructorSignature ';'
  | constantConstructorSignature (redirection | initializers)? ';'
  | constructorSignature (redirection | initializers)? ';'
  | primaryConstructorBodySignature ';'

As introduced by the primary constructor feature, introductory extension type declarations use the primary constructor syntax, but must have precisely one parameter. That parameter must be declaring and final, but it can omit the final keyword. Augmenting extension type declarations cannot write primary constructors.

As with top-level declarations, we also reuse the abstract member syntax with a static modifier to allow declaring incomplete static fields, methods, getters, setters, and operators. Example:

class C {
  static abstract int x;   // Incomplete static variable (getter and setter).
  static int get y;        // Incomplete static getter.
  static set z(int value); // Incomplete static setter.
}

Note that the grammar for putting augment before an extension type declaration doesn't allow also specifying a representation field. This is by design. An extension type augmentation always inherits the representation field of the introductory declaration and can't specify it.

Likewise, the grammar for an augmenting mixin declaration does not allow specifying an on clause. Only the introductory declaration permits that. We could relax this restriction if compelling use cases arise.

Enums

For enum declarations, in addition to the augment modifier, we allow declaring an enum (or augmentation of one) with no values. This is useful if the introductory declaration wants to let the augmentation fill in all values, or if the augmentation wants to add members but no values.

When there are no values, the enum still requires a leading ; before the first member to avoid ambiguity.

enumType ::=
    'augment'? 'enum' classNameMaybePrimary mixins? interfaces? enumBody

enumBody ::=
    '{' (enumEntry (',' enumEntry)* (',')?)? (';' memberDeclarations)? '}'
  | ';'

Note that an enum can also have neither values nor members and both {} and {;} are valid.

Extensions

Extension declarations can be augmented:

extensionDeclaration ::=
    'extension' typeIdentifierNotType? typeParameters? 'on' type
    memberedDeclarationBody
  | 'augment' 'extension' typeIdentifierNotType typeParameters?
    memberedDeclarationBody

Note that only extensions with names allow a leading augment. Since augmentations are matched with their introductory declaration by name, unnamed extensions can't be augmented. Doing so wouldn't accomplish anything anyway. Just make two separate unnamed extensions.

Also note that an augmentation of an extension can't specify an on clause. It always uses the same on clause as the introductory declaration.

Primary constructors

A class, enum or extension type declarationscan use the primary constructor syntax for declaring an initializing (non-redirecting generative) constructor.

See Generative constructors.

Instance variable initializer expressions

If a class or enum has a primary constructor, then the current scope of the initializer expression of a non-late instance variable is the primary initializer scope, rather than the body scope of the surrounding class or enum declaration.

It's a compile-time error if a non-late instance variable initializer expressions refers to a variable introduced by the constructor's initializer list scope, and the surrounding class or enum declaration does not have a primary constructor declaration which declares the corresponding parameter's name. A primary constructor must declare all parameters, but it can omit declaring a positional parameter's name by using _ instead of the name. If it does so, that parameter's name cannot be used by instance variable initializers of that class or enum declaration.

The scope exists and is used whether this particular declaration writes a primary constructor or not, but for readability reasons, code may only refer to variables of that scope, if there appears to be a declaration of the variable's name in the surrounding code.

Static semantics

Augmentation context

Prior to this proposal, an entity like a class or function is introduced by a single syntactic declaration. With augmentations, an entity may be composed out of multiple declarations, the introductory one and any number of augmentations. We define a notion of a augmentation context to help us talk about the location where we need to look to collect all of the declarations that define some entity.

  • The augmentation context of a top-level declaration is the library and its associated tree of part files.

  • The augmentation context of a member declaration in a type or extension declaration named N is the set of type declarations (introductory and augmenting) named N in the enclosing set of Dart files.

Note that augmentation context is only defined for the kinds of declarations that can be augmented. We don't define an augmentation context for, say, local variable declarations, because those aren't subject to augmentation.

Scoping

The static and instance member namespaces for an augmented type or extension declaration include the declarations of all members in the introductory and augmenting declarations. Identifiers in the bodies of members are resolved against that complete merged namespace. In other words, augmentations are applied before identifiers inside members are resolved.

It is already a compile-time error for multiple declarations to have the same name in the same scope. This error is checked after part files and augmentations have been applied. In other words, it's an error to declare the same top-level name in a library and a part, the same top-level name in two parts, the same static or instance name inside an introductory declaration and an augmentation on that declaration, or the same static or instance name inside two augmentations of the same declaration.

For example:

// Library "main.dart":
part 'other.dart';

const name = 'top level';

class C {
  test() {
    print(name);
  }
}

main() {
  C().test();
}

// Part file "other.dart":
part of 'main.dart';

augment class C {
  String get name => 'member';
}

This program prints "member", not "top level". When name is resolved inside test() it walks up to the instance member scope for C. Since that scope contains the merged members of all applied augmentations, it finds the name getter added by the augmentation and uses that instead of continuing and finding the top level name.

You can visualize the namespace nesting sort of like this:

main.dart               : other.dart
                        :
.-----------------------------------------------.
| main.dart imports:                            |
'-----------------------------------------------'
           ^            :           ^
           |            :           |
           |            : .---------------------.
           |            : | other.dart imports: |
           |            : '---------------------'
           |            :           ^
           |            :           |
.-----------------------------------------------.
| top-level declarations:                       |
| const name                                    |
| class C                                       |
'-----------------------------------------------'
           ^            :           ^
           |            :           |
.-----------------------------------------------.
| class C instance members:                     |
| test()                                        |
| name                                          |
'-----------------------------------------------'
           ^            :           ^
           |            :           |
.---------------------. : .---------------------.
| test() body         | : | name body           |
'---------------------' : '---------------------'

The main library file has an import scope which is inherited by all of the part files. Each part file then has its own import scope (which are inherited by that part file's own further part files).

The main library and all part files share and contribute to a single top-level declaration scope. Each type or extension declaration in there has a scope shared across introductory and augmenting declarations of that type or extension.

Then inside those types and extensions are scopes for the member bodies. Each member has its own scope. When resolving an identifier inside a member, we look in the member body, then up through the scopes whose declarations are merged from all of the augmentations and parts, then through the import scopes which may be different for each part file, and finally to the import scope of the main library.

Type annotation inheritance

An augmenting declaration may omit type annotations for a return type, variable type, parameter type, or type parameter bound. In the last case, that includes omitting the extends keyword. For a variable, a var keyword replaces the type if the variable isn't final.

If a type annotation or type parameter bound is omitted in the augmenting declaration, it is inferred to be the same as the corresponding type annotation or type parameter bound in the declaration being augmented.

If the type annotation or type parameter bound is not omitted, then it's a compile-time error if the type denoted by the augmenting declaration is not the same type as the type in the corresponding declaration being augmented.

In short, an augmenting declaration can omit type annotations, but if it doesn't, it must repeat the type from the augmented definition.

Inheriting combined getter setter signatures

An instance getter and instance setter can be augmented with an abstract variable declaration because the latter is syntactic sugar for an abstract getter and setter declaration. This leads to a tricky edge case where the augmenting abstract variable may want to inherit a type but the getter and setter it inherits from have different types:

class C {
  int get x => 1;
  set x(String value) {}

  @metadataToAdd
  augment abstract var x; // What type is inherited here?
}

It's a compile-time error if an abstract variable augments a getter and setter that don't have a combined signature.

Applying augmentations

An augmentation declaration D is a declaration marked with the built-in identifier augment. We add augment as a built-in identifier as a language versioned change, to avoid breaking pre-feature code.

D augments a declaration I with the same name and in the same augmentation context as D. There may be multiple augmentations in the augmentation context of D. More precisely, I is the declaration before D and after every other declaration before D. (See "Application order" for the definition of before and after.)

It's a compile-time error if there is no matching declaration I. In other words, it's an error to have a declaration marked augment with no declaration to apply it to.

We say that I is the declaration which is augmented by D.

In other words, take all of the declarations with the same name in some augmentation context, order them according to the "after" relation, and each augments the result of all the prior augmentations applied to the original declaration. The first one must not be marked augment and all the subsequent ones must be.

An augmentation declaration does not introduce a new name into the surrounding scope. We could say that it attaches itself to the existing name.

It's a compile-time error if an augmentation doesn't have the same kind as the introductory declaration. For example, augmenting a class with a mixin, an enum with a function, a method with a getter, etc.

The exception is that a variable declaration (introductory or augmenting) is treated as a getter declaration (and a setter declaration if non-final) for purposes of augmentation. These implicit declarations can augment and be augmented by other explicit getter and setter declarations. In other words, variables are never augmented or augmenting, only the getters and possibly setters that they induce are.

Complete and incomplete declarations

Augmentations aren't allowed to replace code, so they mostly add entirely new declarations to the surrounding type. However, function and constructor augmentations can fill in a body for an augmented declaration that lacks one.

More precisely, a function or constructor declaration (introductory or augmenting) is incomplete if all of:

  • It has no body. That means no { ... } or => ...; but only ;.

  • The function is not marked external. An external function is considered to have a body, just not one that is visible as Dart code.

  • There is no redirection, initializer list, initializing formals, field parameters, or super parameters. Obviously, this only applies to constructor declarations.

If a declaration is not incomplete then it is complete.

It's a compile-time error if an augmentation is complete and any declaration before it in the augmentation chain is also complete. In other words, once a declaration has acquired a body, no augmentation can replace it with another.

It is allowed to augment a complete declaration as long as the augmentation itself is incomplete. This can be useful for an augmentation to add metadata.

Examples:

a() {}
augment a() {} // Error.

b();
augment b() {} // OK.

c() {}
@meta
augment c(); // OK.

d() {}
augment d(); // OK.
augment d() {} // Error.

Note that the initializer list and body are not treated separately. If a constructor declaration has an initializer list and ; body, it is still considered complete. Likewise, a constructor with no initializer list but a non-; body is complete. Thus a constructor can't acquire an initializer list in one declaration and a constructor body in another. For example:

class C {
  C() : assert(true);
}

augment class C {
  augment C() { body; } // Error. C() is already complete.
}

Application order

The same declaration can be augmented multiple times by separate augmentation declarations. This occurs in the situation where an augmentation declaration has an augmented declaration which is itself an augmentation declaration, and so on, until an introductory declaration is reached.

In some cases (enum values, with clauses, etc.), the order that augmentations are applied is user-visible, so must be specified. Within a single file, the obvious order for applying augmentations is based on which appears first. Expanded part files make that more complex: there may be augmentations of the same declaration scattered across an entire tree of part files.

Some terminology:

  • The source code of a declaration is the span of characters from the first non-whitespace character of the declaration to the last non-whitespace character of the declaration, as matched by the relevant grammar production. Metadata is not considered part of the declaration, it precedes the declaration in the source.

  • A syntactic declaration, D, occurs in a Dart file F if D's source code occurs in that file.

  • A Dart file, F, includes a part file P if the Dart file F has a part directive whose URI denotes P.

  • A Dart file, F, contains (aka. transitively includes) a part file P if F (directly) includes P, or if F includes a part file Q and Q contains P.

  • A Dart file contains a declaration D if the declaration D occurs in the file F itself, or if D occurs in a file P and F contains P.

For any two distinct syntactic declarations A and B:

  • If A and B occur in the same file:

    • If A's position is before B's position in the source code of the file, then A is before B.

      The position of a declaration is defined by the first of these rules that apply:

      • If the declaration is a constructor declaration with no class name, its position is the start position of the new or factory keyword in the source.
      • If the declaration contains an identifier for the declared name, or a qualified identifier for a constructor name, the position is the start position of that identifier in the source. (Includes an unnamed local variable declared with a _ as identifier. A primary constructor declaration's position is that of the class name.)
      • If the declaration is an unnamed extension declaration, its position is the position of the on keyword.

      You cannot augment an unnamed extension declaration, because an augmentation needs to repeat the same name.

    • If B's position is before A's position, then B is before A.

    • If A's and B's position are the same, but they are not the same declaration, then A and B must be a primary constructor declaration and its surrounding type-introducing declaration. In that case, the type introducing declaration is before the primary constructor declaration. (The same effect can be achieved by making the position of a primary constructor be at the end of the class name identifier instead of at the start.)

  • Else if the file where A occurs contains the file where B occurs then A is before B.

  • Else if the file where B occurs contains the file where A occurs then B is before A.

    In other words, if there is a part chain from the file where one declaration occurs to the file where the other occurs, then the outer one comes first.

  • Otherwise, A and B are in sibling branches of the part tree:

    • Let F be the unique Dart file which contains both A and B, but which does not include a part file which also contains both A and B. Such a file must exist for any two declarations because a declaration only exists in one file, and the part files form a tree. Neither A nor B will occur directly in F because if they did, then the previous clauses would have handled it.*

    • If the part directive in F including the file that contains A is syntactically before the part directive in F including the file that contains B in source order, then A is before B.

    • Otherwise B is before A.

    In other words, augmentations in sibling branches are ordered by the part directive order in the file where the branches split off.

We say that B is after A if and only if A is before B.

In short, declarations are ordered by a pre-order depth-first traversal of the file tree, visiting declarations of a file in source order, and then recursing into part directives in source order.

Augmentations are applied in least to greatest order using the after relation.

For example:

// main.dart:
part 'a.dart';
part 'b.dart';

enum E { v1 }
augment enum E { v2 }
augment enum E { v3 }

// a.dart:
augment enum E { v4 }

// b.dart:
augment enum E { v5 }

The resulting enum has values v1, v2, v3, v4, and v5, in that order.

A consequence of application order is that an augmentation in one part file can augment an introductory declaration in a sibling part file. This may be confusing since the augmentation affects a declaration that can't be found anywhere in its part of chain.

However, restricting augmentations to only apply to declarations above (not before) the augmentation would complicate code generators and potentially require them to generate one part file per input part file instead of one part file for the entire library. To avoid that, the rule for where the introductory declaration can be found is intentionally loose so that a code generator can produce one part file which is appended to the main library and which is able to reliably augment any declaration in the entire library regardless of what part file they are introduced in.

Augmenting class-like declarations

A class, enum, extension, extension type, mixin, or mixin class declaration can be marked with an augment modifier:

augment class SomeClass {
  // ...
}

Mixin application classes can't be augmented:

class C = S with M;
augment class C = S with N; // Error.

A class, enum, extension type, mixin, or mixin class augmentation may specify extends, implements and with clauses (when otherwise supported). The types in these clauses are appended to the introductory declarations' clauses of the same kind, and if that clause did not exist previously, then it is added with the new types.

Example:

class C with M1 implements I1 {}
augment class C with M2 implements I2 {}

// Is equivalent to:
class C with M1, M2 implements I1, I2 {}

Instance or static members defined in the body of the augmenting type, including enum values, are added to the instance or static namespace of the corresponding type in the introductory declaration. In other words, the augmentation can add new members to an existing type.

An instance or static member inside a class-like declaration may itself be an augmentation. In that case, it augments the corresponding member (the introducing member with the same name) in the same augmentation context, according to the rules in the following subsections.

It's a compile-time error if:

  • An augmenting class declaration has an extends clause and any prior declaration for the same class also has an extends clause.

  • The augmenting declaration and augmented declaration do not have all the same modifiers: abstract, base, final, interface, sealed and mixin for class declarations, and base for mixin declarations.

    This is not a technical requirement, but follows our design principle that what is known from reading the introductory declaration will still be true after augmentation.

  • The type parameters of the augmenting declaration do not match the introductory declarations's type parameters. This means there must be the same number of type parameters with the exact same type parameter names (same identifiers) and bounds if any (same types, even if they may not be written exactly the same in case one of the declarations needs to refer to a type using an import prefix).

    Since repeating the type parameters is, by definition, redundant, this restriction doesn't accomplish anything semantically. It ensures that anyone reading the augmenting type can see the declarations of any type parameters that it uses in its body and avoids potential confusion with other top-level variables that might be in scope in the library augmentation.

    The augmenting declaration may choose to omit the bound on any type parameter, in which case it will be inherited from the introductory declaration.

    foo<X extends num, Y extends X>();
    augment foo<X extends num, Y>() { ... }
    

    Here, Y in the augmentation inherits extends X from the introductory declaration.

Augmenting function and constructor signatures

When augmenting a function (top level, static method, instance method, etc.) or constructor (generative, factory, etc.) the parameter lists must be the same in all meaningful ways. We say that an augmenting function or constructor's signature matches an introductory signature if:

  • It has the same number of type parameters with the same type parameter names (same identifiers) and bounds (after type annotation inheritance), if any (same types, even if they may not be written exactly the same in case one of the declarations needs to refer to a type using an import prefix).

  • The return type (if not omitted) is the same as the introductory declaration's return type.

  • It has the same number of positional parameters as the introductory declaration, and the same number of those are optional.

  • It has the same set of named parameter names as the introductory declaration.

  • For each corresponding pair of parameters:

    • They have the same type (or the augmenting declaration omits the type).

    • They both have the modifier covariant, or none of them have it.

    • They both have the modifier required, or none of them have it.

    For constructors, we do not require parameters to match in uses of initializing formals or super parameters. In fact, they are implicitly prohibited from doing so: if a constructor and its augmentation both have initializing formals or super parameters, they are both complete and it's an error to augment a complete constructor with another complete constructor. Instead, at most only one of the constructors can use initializing formals or super parameters and all other declarations for the same constructor must declare the corresponding parameters as regular parameters.

  • For all positional parameters:

    • The name of the augmenting parameter declaration is _, or

    • The name of the augmenting parameter declaration is the same as the name of the corresponding parameter declaration in every preceding declaration that doesn't have _ as its name.

    In other words, a declaration can ignore a positional parameter's name by using _, but all declarations in the chain that specify a name which is not _ must agree on it.

    f1(int _) {} // OK, this declaration doesn't care about the name.
    augment f1(int x); // OK, first declaration to introduce a name.
    augment f1(int _); // OK.
    augment f1(int y); // Error, can't change the name.
    augment f1(int _); // OK.
    

    *If an augmentation specifies _ as the name of a parameter, a non-_ name is not "inherited" from a preceding declaration for use in the augmentation's body. The name of the parameter for that augmentation is _, which can't be accessed because it's a wildcard:

    f(int x);
    augment f(int _) { print(x); } // Error.
    

In this definition, the properties of the introductory declaration may correspond to an explicitly declared property (such as an explicitly stated return type) or an inferred property (such as a parameter type which has been obtained by override inference).

In a declaration where a parameter named n is declared using _, the name n is not in scope and may be resolved elsewhere. For example:

int y = 42;

void g(int y);

augment g(_) {
  print(y); // OK, prints '42'.
}

Augmenting functions

A top-level function, static method, instance method, operator, getter, or setter may be augmented to provide a body or add metadata:

class Person {
  final String name;
  final int age;

  Map<String, Object?> toJson();
}

// Provide a body for `toJson()`:
augment class Person {
  augment toJson() => {'name': name, 'age': age};
}

A top-level function, static method, or instance method may be augmented to provide default values for optional parameters:

class C {
  void m1([int i]);
  void m2({String name});
  void m3({String otherName = "Smith"}); // OK, too.
}

augment class C {
  augment m1([i = 1]) {}
  augment m2({name = "John"}) {}
  augment m3({otherName}) {}
}

An optional formal parameter has the default value d if exactly one declaration of that formal parameter in the augmentation chain specifies a default value, and it is d. An optional formal parameter does not have an explicitly specified default value if none of its declarations in the augmentation chain specifies a default value. The default value is introduced implicitly with the value null in the case where the parameter has a nullable declared type, and no default values for that parameter are specified in the augmentation chain.

It's a compile-time error if:

  • The signature of the augmenting function does not match the signature of the corresponding introductory declaration.

  • More than one declaration in the augmentation chain specifies a default value for the same optional parameter. This is an error even in the case where all of them are identical. Default values are defined by the introductory function or an augmentation, but at most once.

  • No declaration in the augmentation chain specifies a default value for an optional parameter whose declared type is potentially non-nullable, and the declared function is not abstract.

  • A function is not complete after all augmentations are applied, unless it's an instance member and the surrounding class is abstract. Every function declaration eventually needs to have a body filled in unless it's an instance method that can be abstract. In that case, if no declaration provides a body, it is considered abstract.

Augmenting variables, getters, and setters

For purposes of augmentation, a variable declaration is treated as implicitly defining a getter whose return type is the type of the variable. If the variable is not final, or is late without an initializer, then the variable declaration also implicitly defines a setter with a parameter named _ whose type is the type of the variable.

If the variable is abstract, then the getter and setter are incomplete, otherwise they are complete. For non-abstract variables, the compiler synthesizes a getter that accesses the backing storage and a setter that updates it, so these members have bodies.

A getter can be augmented by another getter, and likewise a setter can be augmented by a setter. This is true whether the getter or setter is explicitly declared or implicitly declared using a variable declaration.

Since non-abstract variables are complete, that implies that it is an error to augment a non-abstract variable declaration with a complete getter, setter, or variable declaration. Likewise, it is an error to augment a complete getter or setter with a non-abstract variable declaration.

It's a compile-time error if:

  • The signature of the augmenting getter or setter does not match the signature of the corresponding introductory getter or setter.

  • A const variable declaration is augmented or augmenting.

  • A getter or setter (including one implicitly induced by a variable declaration) is not complete after all augmentations are applied, unless it's an instance member and the surrounding class is abstract. Every getter or setter declaration eventually needs to have a body filled in unless it's an instance member that can be abstract. In that case, if no declaration provides a body, it is considered abstract.

Using the general compile-time error principle, some additional situations are errors. In particular, a compile-time error occurs if, after application of all augmentations, a static or library variable has no initializing expression, and its type is not nullable, and the declaration does not have the modifier late nor the modifier external.

Augmenting enums

An augmentation of an enum type can add new members to the enum, including new enum values. Enum values are appended in augmentation application order.

Enum values themselves can't be augmented since they are essentially constant variables and constant variables can't be augmented.

An introductory enum declaration introduces implicit introductory and complete declarations of:

  • int get index
  • int get hashCode
  • bool operator ==(Object)
  • static const List<E> values; where E is the enum type.

For ordering purposes, these implicit declarations are before any members declared in the declaration.

Any declaration of the same members must be augmenting (they are not introductory) and must not be complete. Declaring an instance member named values will conflict with the static values declaration as a normal scope name conflict.

It's a compile-time error if:

  • An enum doesn't have any values after all augmentations are applied. The grammar allows an enum declaration to not have any values so that other declarations of the same enum can add them, but ultimately the enum must end up with at least one value.

Augmenting constructors

Augmenting a constructor works similarly to augmenting a function, with some extra rules to handle features unique to constructors, like redirections and initializer lists, and the primary constructor syntax.

A constructor declaration is a factory constructor declaration if it has a factory keyword, and it is a generative constructor declaration if it does not, including when it is a primary constructor declaration.

A constructor declaration is a const constructor declaration if it has a const keyword, or if it is a generative constructor declaration of an enum declaration, and otherwise it is not. For a primary constructor, the const keyword goes before the class name in the header.

Factory constructor declarations

A factory constructor declaration is a non-redirecting factory constructor declaration, and is a complete declaration, if it has a body ({...} or => ...;) or an external keyword.

A factory constructor declaration is a redirecting factory constructor declaration and is complete if it has a redirection clause (= TargetType;).

A factory constructor declaration with no body and no redirection clause, ended by a single ;, is not a complete declaration, and does not decide whether the factory constructor being defined is redirecting or not. Only the (single) complete declaration forces that decision.

Generative constructor declarations.

A primary constructor declaration causes the constructor it defines to be a primary constructor, and the class to have a primary constructor. This means the class cannot have any other initializing constructors, which is reflected by the consistency rules below, and it changes the scope used for instance variable initializer expressions.

A primary constructor declaration always has an in-header part which contains a parameter list, and optionally a const and a constructor- name identifier, (class const C.id(int x)). It may also have an in-body this-part which can contain an initializer list or body, and can have metadata attached (this: super(x) { print('done'); }).

A primary constructor declaration is an initializing constructor declaration. A primary constructor declaration is complete if (any of):

  • It has an initializing formal parameter (this.name).
  • It has a super parameter (super.name).
  • It has a declaring parameter (final int name).
  • It has an in-body this-part that:
    • has a body ({...}) and/or
    • has an initializer list (: ...).
  • It's in an extension type declaration. A primary constructor declaration is an augmenting declaration if (either of):
  • It has an in-body this-part with an augment keyword (augment this ...).
  • It has no in-body this-part, and there is another constructor declaration with the same name which occurs before this constructor declaration. Otherwise it is an introductory declaration. An augmenting primary constructor with no in-body this-part does not have a separate augment keyword. Such a constructor is necessarily part of an augmenting class or enum declaration, and its header-part is likely on the same line as the augment keyword of that declaration. This is considered sufficient, rather than forcing the author of a class declaration like augment class C(final int x); to add a body and in-body this-part just to write an augment on it, or alternatively to require a second augment keyword in the header as augment class augment C(final int x);.

If a complete primary constructor has a declaring parameter, that parameter declaration also counts as a complete introductory or augmenting variable declaration, which is final if the declaring parameter has the final modifier, and is augmenting if there is a prior declaration of the same getter and/or setter. (The augment modifier does not apply to parameters, so the declaring parameter cannot be marked as augmenting a variable.)

A non-primary generative constructor declaration is an initializing constructor declaration and is complete if (any of):

  • It has an external keyword.
  • It has an initializing formal parameter (this.name).
  • It has a super parameter (super.name).
  • It has a body ({...} instead of ;).
  • It has an initializer list (: ...).

(A non-primary initializing constructor cannot have declaring parameters, those are only available to primary constructor declarations. If declaring parameters ever become valid for a non-primary generative constructor, they'll also make the constructor complete.)

A non-primary generative constructor declaration is a redirecting generative constructor declaration and is complete if it has a redirection clause (: this(args); or : this.name(args);).

The declarations of a class or enum can contain both primary and non-primary declarations of the same constructor. The class has a primary constructor if at least one declaration uses the primary constructor syntax.

An in-body generative constructor declaration with no special parameters, initializer list, body or redirection, like ClassName(), does not decide whether the constructor is redirecting or initializing. That is decided by any declaration which is complete, or which is a primary constructor since those must be initializing.

If all declarations of a generative constructor are incomplete, (and therefore contains no redirecting generative constructor declarations, since those are all complete), then the constructor being defined is an initializing constructor, which is const if the declarations are, and which has normal parameters corresponding to the signature and default values defined by all the declarations, no initializer list, invoking the "unnamed" superclass constructor with no arguments, and with no body. (A declaration of C(); is defined as incomplete, but is also historically a valid concrete implementation of a trivial constructor, and this ensures that it keeps working that way. Effectively if a generative constructor has no complete declaration, it gets a "default constructor" implementation with normal parameters for the combined parameters declared by all of the incomplete constructors. Those parameters may be visible in instance variable initializers.)

Example:

class C {
  C.generative();
  factory C.fact();

  C.other();
}

augment class C {
  augment C.generative() : this.other();
  augment factory C.fact() = C.other;
}

Here C.other has only incomplete declarations. The class gets an implementation of that constructor equivalent to C.other(): super();.

Example with a primary constructor:

class const D(int x) {
  final int squared = x * x;
  /// Creates a `D`.
  this;
  augment const new(@Since("3.15") int x);
}

augment class const D(final int x) {
  augment this { print("Initialized D"); }

  @Deprecated("Was a bad idea anyway")
  augment const D(int _);
}

Example with incomplete primary constructor.

class const D(int _) {
  /// Don't know yet.
  init;
}
augment class const D(int x) {
  final int squared = x * x;
}

Here the D.new constructor has only incomplete declarations. It gest a default implementation equivalent to D(int x): super();, based on the names and types of all declarations, and it can use x in its instance variable declarations (which is exactly how the second class declaration would work by itself without augmentations).

Consistency rules for constructor declarations

It's a compile-time error if the declarations of a class or enum contain constructor declarations where:

  • There is an augmenting constructor declaration with no corresponding (earlier) introductory declaration.

  • There is an introductory constructor declaration with any other declaration for that constructor which is before it. Same two rules as for other augmentations. The first declaration must be introductory, any non-first declaration must be augmenting.

  • There exist two constructor declarations for the same constructor, and (any of):

    • Both are complete declarations. (As everywhere else, there can be at most one complete declaration.)
    • One is a const constructor declaration and the other is not.
    • One is a factory constructor declaration and the other is a generative constructor declaration.
    • One is a redirecting factory constructor declaration and the other is a non-redirecting factory constructor declaration. Redundant since both would be complete declarations too, but included for completeness, in case later language features would make it not be redundant.
    • One is an initializing constructor declaration and the other is a redirecting generative constructor declaration.

    If there are more than two declarations of a constructor, and one of them does not match the rest for one of these rules, there is more than one way that that program has a compile-time error. How to report that usefully is up to the tool that checks for errors. It may be sufficient to point to the one declaration that stands out, or it can choose to report every declaration that has an error compared to the introductory declaration.

  • The signature of an augmenting constructor does not match the signature of the corresponding introductory constructor. The signature of a constructor using the privately-named-parameters feature uses the public name for that parameter.

  • There is a primary constructor declaration, and there is an initializing constructor declaration with a different name, and the enclosing declaration is a class or enum declaration. If a class or enum has a primary constructor declaration, that constructor must still be the only initializing constructor, just as specified by the primary-constructors feature. That restriction applies to the entire class or enum being defined, not just a single declaration, and applies in both directions if two separate class declarations have primary constructor declarations for different constructors.

  • Two different declarations of the same constructor both specify a default value of the same parameter. This is an error even if it is the same default value. Parameter default values can be defined by the introductory declaration or by an augmenting declaration, but at most once.

  • A constructor declaration has a default value for a parameter, and there is a redirecting factory constructor declaration for the same constructor. It can be the same declaration. Redirecting factory constructors cannot declare default values.

  • A constructor declaration declares an optional parameter with a non-null type, and no declaration for the same constructor:

    • declares a default value for that parameter, or
    • is a redirecting factory constructor declaration. Non-redirecting factory constructors must have default values for non-nullable optional parameters.
  • There is a factory constructor declaration and no complete constructor declaration for the same constructor. A factory constructor which has ; instead of a body or redirection is incomplete, and it's not deciding whether the constructor is redirecting or not. This allows, and requires due to this rule, another declaration to fill in an implementation. Generative constructors may have no complete declarations, they'll then get a trivial default implementation as described above.

Instance variable initialization during constructor invocation

When invoking an initializing generative constructor to initialize a new object, instance variable initialization happens before executing the initializer list.

This is true whether or not the class or enum has a primary constructor.

When invoking the initializing constructor to initialize a new object, the first thing that happens is that actual arguments are bound to formal parameters, which provides the bindings for the initializer list scope.

Then all non-late instance variable declarations of the class which have an initializer expression are processed in their source order.

Each instance variable is initialized in turn, by evaluating its initializer expression. If the class or enum has a primary constructor, the initializer expression is evaluated in the initializer list scope, otherwise it's evaluated in the body scope of the surrounding class or enum (extension and extension type declarations cannot contain instance variable declarations, mixins and mixin-application classes cannot have primary constructors).

If the class has a constant generative constructor, then it's still a compile- time error if the class has any non-final instance variables, and it's still a compile-time error if an instance variable has an initializer expression that is not a potentially constant expression.

Whether the evaluation uses the body scope or the initializer list scope, which has the body scope as parent scope, it's still an error if the expression refers to any instance member in the body scope.

After all instance variable initializers have been executed, constructor execution continues with executing as in Dart before this feature, starting with the variable initialization of the parameter list, by initializing formals and declaring parameters.

This is how primary constructors already work. The only difference is that because a single constructor can be introduced by more than one declaration, the complete declaration, the actual implementation, of a primary constructor might not be a primary constructor declaration.

Augmenting extension types

An introductory extension type declaration must have a primary constructor clause, which must have precisely one parameter. Just like for a class or enum, that primary constructor clause is a constructor declaration. For an extension type, it is an introductory and complete initializing constructor declaration.

That primary constructor's parameter declaration is also an introductory and complete getter declaration for a getter with the same name and type as the parameter declaration. This is just like a declaring final parameter of a primary constructor of a class declaration. The parameter of an extension type primary constructor is always declaring and final, whether it has an explicit final or not.

This orders these declarations before any member declarations inside the extension type declaration, and since they must be declared by the introductory extension type declaration, they are always introductory.

When augmenting an extension type declaration, the representation type declaration cannot be repeated, an augmenting extension type declaration cannot have a primary constructor.

Note

Since the representation type declaration on the introductory extension type introduces a complete constructor, it is a known limitation that an augmentation cannot later attach an implementation body to the primary constructor. All implementation of the primary constructor must be given in the introductory declaration. To work around this, developers can declare a private primary constructor and expose a public constructor to be augmented.

Augmenting with metadata annotations

An augmenting declaration can have metadata attached to it. The language doesn't specify how metadata is used. Tools may choose to append metadata from augmentations to the resulting combined declaration or allow inspecting the metadata on the individual augmentations.

In practice, most code generators use the analyzer package to introspect over code. Code generators introspecting on the syntax of some code likely want to see the metadata for each syntactic declaration separately. Code generators introspecting over the resolved semantic model of the code (which is more common) probably want to see the metadata of the introductory declaration and all augmentations appended into a single list of metadata accessible from the combined declaration.

Compile-time errors which are eliminated

With this feature, it is no longer an error to have the following situations:

  • An implements clause contains two or more type operands denoting the same type. For example, class A implements I, I; is no longer an error.

  • An implements clause of a declaration D contains an operand T, and an extends or with clause of D contains an operand S, and T and S denote the same type. For example, class A extends I implements I; is no longer an error.

  • An implements clause of a mixin declaration D contains an operand T, and an on clause of D contains an operand S, and T and S denote the same type. For example mixin M on I implements I; is no longer an error.

An operand of an extends, with, on, or implements clause is a type that occurs in the clause which is not a subterm of another type. In particular, these changes have no relevance to nested occurrences like I in class A extends B<I> implements C<I, I Function(String)>; (which were not errors, anyway).

The motivation for these changes is mainly that, in general, compile-time errors are reported for code constructs whose semantics cannot reasonably be determined, and hence no program containing this code can be compiled and executed.

In contrast, redundant implemented interfaces do not give rise to any ambiguities or inconsistencies. The errors were only reported because the situation was considered useless and confusing, and it was assumed to arise only by mistake.

It is outside the scope of a language specification document like this one, but it may well be useful for tools like the analyzer or linter to report a warning if this kind of redundancy is detected. They might treat redundancies in the same syntactic construct more severely than redundancies that only exist in the semantic declaration, but are syntactically located in different elements of an augmentation chain.

Compile errors with augmentations

Prior to augmentations, the definition of a semantic entity is produced by a single syntactic declaration. That allows the language specification to refer to those entities interchangeably. With augmentations, that is no longer the case. A single semantic entity may be the product of multiple syntactic declarations (an introductory and any number of augmentations). This raises the question of whether existing compile errors apply to syntactic declarations or semantic definitions.

For example, it is an error according to the language specification if a concrete class has an abstract instance member declaration D, and there is no implementation inherited from a superclass whose signature is a correct override of the member signature of D.

Thus this is an error in Dart without augmentations:

class C {
  int get g;
}

However, when augmentations are supported it is possible to provide the missing implementation in an augmenting declaration:

augment class C {
  augment int get g => 0;
}

The general rule is that compile-time errors apply to semantic definitions whenever possible. For this example it is the class as a whole, as defined by all its introducing and augmenting declarations, which must have a concrete member whose signature is a valid override for its interface member signature. Having some individual member declarations which do not introduce an implementation does not mean that the class does not introduce an implementation of that member, as long as there is one concrete member declaration for the member.

The motivation for this principle is that reorganizing code into or out of augmentations shouldn't affect the errors that are reported. Augmentations are a syntactic tool for organizing code, but what the user cares about -- and what static analysis should thus focus on -- is the semantics of the resulting definitions. Also, in most cases the error relies on semantic information that isn't even well defined for syntactic entities and is only known from the resolved semantic definition which can't be produced without applying augmentations.

Dynamic semantics

The application of augmentation declarations to an augmented declaration produces something that looks and behaves like a single declaration: It has a single name, a single type or function signature, and it's what all references to the name refers to inside and outside of the library.

Unlike before, that single semantic declaration now consists of multiple syntactic declarations (one introductory declaration, the rest augmenting declarations, with a given augmentation application order), and the properties of the combined semantic declaration can be derived from the syntactic declarations.

We redefine a number of semantic functions to now work on a stack of declarations (the declarations for a name in bottom to top order), so that existing semantic definitions keep working.

Example: Class declarations

Super-declarations

The specification of class modifiers introduced a number of predicates on declarations, to check whether the type hierarchy is well formed and the class modifiers are as required, before the static semantics have even introduced types yet. We modify those predicates to apply to a stack of augmenting declarations and an introductory declaration as follows:

  • A a non-empty stack of syntactic class declarations, C, has a declaration D as declared super-class if:
    • C starts with an (augmenting or not) class declaration C0 and either
      • C0 has an extends clause whose type clause denotes the declaration D, or
      • C0 is an augmenting declaration, so C continues with a non-empty Crest, and Crest has D as declared super-class.
  • A a non-empty stack of syntactic class declarations, C, has a declaration D as declared super-interface if:
    • C starts with an (augmenting or not) class declaration C0 and either
      • C0 has an implements clause with an entry whose type clause denotes the declaration D, or
      • C0 is an augmenting declaration, so C continues with a non-empty Crest, and Crest has D as declared super-interface.
  • A a non-empty stack of syntactic class declarations, C, has a declaration D as declared super-mixin if:
    • C starts with an (augmenting or not) class declaration C0 and either
      • C0 has a with clause with an entry whose type clause denotes the declaration D, or
      • C0 is an augmenting declaration, so C continues with a non-empty Crest, and Crest has D as declared super-mixin.

Members

A class declaration stack, C, of an introductory declaration and zero or more augmenting declarations, defines an augmented interface (member signatures) and augmented implementation (instance members declarations) based on the individual syntactic declarations.

A non-empty class declaration stack, C, has the following set of instance member declarations:

  • Let Ctop be the latest declaration of the stack, and Crest the rest of the stack.
  • If Ctop is a non-augmenting declaration, the declarations of C is the set of syntactic instance member declarations of Ctop.
  • Otherwise let P be the set of member declarations of the non-empty stack Crest.
  • and the member declarations of C is the set R defined as containing only the following elements:
    • A singleton stack of each syntactic instance member declaration M of Ctop, where M is a non-augmenting declaration.
    • The elements N of P where Ctop does not contain an augmenting instance member declaration with the same name (mutable variable declarations have both a setter and a getter name).
    • The stacks of a declaration M on top of the stack N, where N is a member of P, M is an augmenting instance member declaration of Ctop, and M has the same name as N.

And we can whether such an instance member declaration stack, C, defines an abstract method as:

  • Let Ctop be the latest element of the stack and Crest the rest of the stack.
  • If Ctop is a non-variable declaration, and is not declared abstract, the C doe
  • If Ctop declares a function body, then C does not define an abstract method.
  • Otherwise C defines an abstract method if Crest defines an abstract method.

(This is just for methods, we will define it more generally for members, including variable declarations.)

Example: Instance methods

Properties

Similarly we can define the properties of stacks of member declarations.

For example, we define the augmented parameter list of a non-empty stack, C, of augmentations on an introductory function declaration as:

  • Let Ctop be the latest element of the stack and Crest the rest of the stack.
  • If Ctop is not an augmenting declaration, its augmented parameter list is its actual parameter list. (And Crest is known to be empty.)
  • Otherwise Ctop is an augmenting declaration with a parameter list which must have the same parameters (names, positions, optionality and types) as its augmented declaration, except that it is not allowed to declare default values for optional parameters.
    • Let P be the augmented parameter list of Crest.
    • The augmented parameter list of Ctop is then the parameter list of Ctop, updated by adding to each optional parameter the default value of the corresponding parameter in P, if any.

This will usually be exactly the parameter list of the introductory declaration, but the ordering of named parameters may differ. This is mostly intended as an example, in practice the augmented parameter list can just be the parameter list of the introductory declaration, but it's more direct and clearly correct to use the actual parameter list of the declaration when creating the parameter scope that its body will run in.

Similarly we define the augmented function type of the declaration stack. Because of the restrictions we place on augmentations, they will all have the same function type as the introductory declaration, but again it's simpler to assign a function type to every declaration.

Invocation

When invoking an instance member on an object, the current specification looks up the corresponding implementation on the class of the runtime-type of the receiver, traversing super-classes, until it it finds a non-abstract declaration or needs to search past Object. The specification then defines how to invoke that method declaration, with suitable contexts and bindings.

We still define the same thing, only the result of lookup is not a single declaration, but a stack of augmenting declarations on top of an introductory declaration, and while searching, we skip past declaration stacks that define an abstract method. The resulting stack is the member definition, or semantic declaration, which is derived from the syntactic declarations in the source.

Invoking a stack, C, of instance method declarations on a receiver object o with an argument list A and type arguments T, is then defined as follows:

  • Let Ctop be the latest declaration on the stack (the last applied augmentation in augmentation application order), and Crest the rest of the stack.
  • If Ctop has a function body B then:
    • Bind actuals to formals (using the usual definition of that), binding the argument list A and type arguments T to the augmented parameter list of Ctop and type parameters of Ctop. This creates a runtime parameter scope which has the runtime body scope as parent scope (the lexical scope of the class, except that type parameters of the class are bound to the runtime type arguments of those parameters for the instance o).
    • Execute the body B in this parameter scope, with this bound to o.
    • There would have been a compile-time error if there is no earlier declaration with a body.
    • The result of invoking C is the returned or thrown result of executing B.
  • Otherwise, the result of the invocation of C is the result of invoke Crest on o with argument list A and type arguments T.
    • This will eventually find a body to execute, otherwise C would have defined an abstract method, and would not have been invoked to begin with.

Tooling

Documentation comments

Documentation comments are allowed in all the standard places in library augmentations. It is up to the tooling to decide how to present such documentation comments to the user, but they should generally be considered to be additive, and should not completely override the original comment. In other words, it is not the expectation that augmentations should duplicate the original documentation comments, but instead provide comments that are specific to the augmentation.

Part directive order

The order that augmentations are applied is sometimes user visible. That order is in turn affected by the order of part file directives in the file.

That means that the order of part declarations in a file is now semantically meaningful. Tools like IDEs should not assume it is always safe to, say, automatically alphabetize them. However, in most cases, augmentation order doesn't matter and it's usually safe to sort them if a user requests it.

For the part directive order to matter:

  1. There must be augmentations of the same declaration in multiple separate part files.

  2. The part files containing the augmentations must be siblings with neither a parent of the other.

  3. Those augmentations must have their application order be user visible. This isn't defined precisely, but includes adding enum values or mixins (with clause). Even then, the order is often not visible. Both augmentations would have to add enum values. If multiple augmentations add with clauses, the order is only visible if the applied mixins have overlapping members.

The first two are fairly simple to detect. The third is subtle (and may not be fully captured by that paragraph). It's probably safest to be pessimistic and assume the third point is always true.

Changelog

1.44

  • Add primary constructors.

  • Fix the Before/After relation definitions (prior definition used "includes" transitively even if it was only declared for direct part includes).

  • Remove restriction against declaring toString in an enum. There is no existing rule against an enum overriding toString, and augmentations shouldn't introduce a restriction.

  • Change restriction against declaring operator ==, hashCode, index and values in an enum to having the introductory enum introduce those members as complete declarations. Then you can still @something augment get index; to add metadata. (Existing language allows abstract declarations of all mentioned members except values.)

1.43

  • Restore the rule about the use of _ as a "don't care" name in the augmentation chain for a formal parameter, using the same text as in version 1.37.

1.42

  • Adjust the grammar to allow empty membered bodies to be specified as a semicolon.

1.41

  • Adjust the grammar to enforce that some top-level constant and final variable declarations have an initializing expression.

  • Add support for static abstract variable declarations in the grammar.

  • Adjust the definition of signature matching (using _ as "don't care" parameter names is no longer supported).

  • Add a section to say that certain redundant superinterfaces are no longer an error.

1.40

  • Clarify how applying augmentations interacts with compile-time errors (#3690).

1.39

  • Non-semantic copy editing. Remove some redundant specification. Clarify that generative constructors can remain incomplete without error.

  • Add some rationale for why the introductory declaration only needs to be before the augmenting ones and not above (#4377).

1.38

  • Generalize the treatment of default values of optional parameters (#4172).

1.37

  • Rename to "augmentations" (from "augmentation libraries") and define the experiment flag to be "augmentations" (was part of "macros").

1.36

  • Remove augment from typedef grammar since typedefs can no longer be augmented (#4388).
  • Allow augmenting variable declarations (#4387).

1.35

  • Reorganize sections.
  • Remove references to macros.
  • Don't allow augmentations to wrap or replace code. Remove support for augmented expressions. Disallow an augmentation from providing a body to a declaration that already has one.
  • Remove support for augmenting variables.
  • Simplify constructor augmentations: no concatenating initializers or merging initializers from one augmentation and a body from another.
  • Remove support for augmenting typedefs.
  • Remove support for augmenting redirecting constructors.
  • Allow a function augmentation to have an external body.
  • Rewrite "Scoping" section to be clearer.
  • Remove recommend path ordering lint. Commit to making part directive order meaningful and acceptable to rely on (#3849).
  • Allow enum declarations without values (#4356).
  • Specify signature matching for implicit setters from abstract variables (#4022).
  • Clarify that you can't augment an extension type constructor and add a body (#4047).
  • Don't allow augmenting mixin application classes (#4060).

1.34

  • Revert some errors introduced in version 1.28.

    • An abstract variable can now be augmented with non-abstract getters and setters.
    • External variables can now be augmented with abstract getters and setters.

1.33

  • Change the grammar to remove the primary constructor parts of an augmenting extension type declaration.

1.32

  • Specify that variables which require an initializer can have it defined in any augmentation.
  • Specify that the implicit null initialization is not applied until after augmentation.

1.31

  • Specify that it is an error to have a static and instance member with the same name in the fully merged declaration.

1.30

  • Simplify extension type augmentations, don't allow them to contain the representation type at all.

1.29

  • Simplify enum value augmentations, no longer allow altering the constructor invocation.

1.28

  • Explicitly disallow augmenting abstract variables with non-abstract variables, getters, or setters.
  • Explicitly disallow augmenting external declarations with abstract declarations.
  • Remove error when augmenting an abstract or external variable with a variable (allowed for adding comments/annotations).

1.27

  • Specify that representation objects for extension types cannot be augmented.

1.26

  • Recreate the change made in 1.23 (which was undone by accident).

1.25

  • Clarify that augmentations can occur in the same type-introducing declaration body, even in a non-augmenting declaration.
  • Update some occurrences of old terminology with new terms.

1.24

  • Allow augmentations which only alter the metadata and/or doc comments on various types, and specify behavior.

1.23

  • Change augmented operator invocation syntax to be function call syntax.

1.22

  • Unify augmentation libraries and parts. Parts with imports moved into separate document, as a stand-alone feature that is not linked to augmentations.
  • Augmentation declarations can occur in any file, whether a library or part file. Must occur "below" the introductory declaration (later in same file or sub-part) and "after" any prior applied augmentation that it modifies (below, or in a later sub-part of a shared ancestor).
  • Suggest a stronger ordering lint, where the augmentation must be "below" the augmentation it is applied after. That imples that all declarations with the same name are on the same path in the library file tree, so that reordering part directives does not change augmentation application order.
  • Change the lexical scope of augmenting class-like declarations to only contain the member declarations that are syntactically inside the same declaration, rather than collecting all member declarations from all augmenting or non-augmenting declarations with the same name, and making them all available in each declaration.
  • Avoid defining a syntactic merging, since it requires very careful scope management, which isn't necessary if we can just extend properties that are currently defined for single declarations to the combination of a declaration plus zero or more augmentations.

1.21

  • Add a compile-time errors for wrong usages of augmented.

1.20

  • Change the extensionDeclaration grammar rule such that an augmenting extension declaration cannot have an on clause. Adjust other rules accordingly.

1.19

  • Change the phrase 'augmentation library' to 'library augmentation', to be consistent with the rename which was done in 1.15.

1.18

  • Add a grammar rule for enumEntry, thus allowing them to have the keyword augment.

1.17

  • Introduce compile-time errors about wrong structures in the graph of libraries and augmentation libraries formed by directives like import and import augment (#3646).

1.16

  • Update grammar rules and add support for augmented type declarations of all kinds (class, mixin, extension, extension type, enum, typedef).

  • Specify augmenting extension types. Clarify that primary constructors (which currently only exist for extension types) can be augmented like other constructors (#3177).

1.15

  • Change library augment to augment library.

1.14

  • Change augment super to augmented.

1.13

  • Clarify which clauses are (not) allowed in augmentations of certain declarations.
  • Allow adding an extends clause in augmentations.

1.12

  • Update the behavior for variable augmentations.

1.11

  • Alter and clarify the semantics around augmenting external declarations.
  • Allow non-abstract classes to have implicitly abstract members which are implemented in an augmentation.

1.10

  • Make augment a built-in identifier.

1.9

  • Specify that documentation comments are allowed, and should be considered to be additive and not a complete override of the original comment. The rest of the behavior is left up to implementations and not specified.

1.8

  • Specify that augmented libraries and their augmentations must have the same language version.

  • Specifically call out that augmentations can add and augment enum values, and specify how that works.

1.7

  • Specify that augmentations must contain all the same keywords as the original declaration (and no more).

1.6

  • Allow class augmentations to use different names for type parameters. This isn't particular valuable, but is consistent with functions augmentations which are allowed to change the names of positional parameters.

  • Specify that a non-augmenting declaration must occur before any augmentations of it, in merge order.

  • Specify that augmentations can't have parts (#2057).

1.5

  • Augmentation libraries share the same top-level declaration and private scope with the augmented library and its other augmentations.

  • Now that enums have members, allow them to be augmented.

  • Compile-time error if a non-late augmenting instance variable calls the initializer for a late one.

1.4

  • When inferring the type of a variable, only the original variable's initializer is used.

1.3

  • Constructor and function augmentations can't define default values.

1.2

  • Specify that augmenting constructor initializers are inserted before the original constructor's super or redirecting initializer if present (#2062).
  • Specify that an augmenting type must replicate the original type's type parameters (#2058).
  • Allow augmenting declarations to add metadata annotations and macro applications (#2061).

1.1

  • Make it an error to apply the same augmentation multiple times (#1957).
  • Clarify type parameters and parameter modifiers in function signature matching (#2059).

1.0

Initial version.