[dart2js] `late` annotations with context

Annotations

    @pragma('dart2js:late:trust')
    @pragma('dart2js:late:check')

are now scoped, so that an annotation on a library or class will take effect unless shadowed by a closer `dart2js:late` annotation.
Currently the annotations affect only `late` fields.

Change-Id: Ida5c2b9387449263e29e74925aac9172fb3dc40c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/251503
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
diff --git a/pkg/compiler/lib/src/ir/annotations.dart b/pkg/compiler/lib/src/ir/annotations.dart
index 11c80ec..7594410 100644
--- a/pkg/compiler/lib/src/ir/annotations.dart
+++ b/pkg/compiler/lib/src/ir/annotations.dart
@@ -368,14 +368,15 @@
   return null;
 }
 
-List<PragmaAnnotationData> computePragmaAnnotationDataFromIr(ir.Member member) {
+List<PragmaAnnotationData> computePragmaAnnotationDataFromIr(
+    ir.Annotatable node) {
   List<PragmaAnnotationData> annotations = [];
-  for (ir.Expression metadata in member.annotations) {
+  for (ir.Expression metadata in node.annotations) {
     if (metadata is! ir.ConstantExpression) continue;
     ir.ConstantExpression constantExpression = metadata;
     ir.Constant constant = constantExpression.constant;
     assert(constant is! ir.UnevaluatedConstant,
-        "Unexpected unevaluated constant on $member: $metadata");
+        "Unexpected unevaluated constant on $node: $metadata");
     PragmaAnnotationData? data = _getPragmaAnnotation(constant);
     if (data != null) {
       annotations.add(data);
diff --git a/pkg/compiler/lib/src/js_backend/annotations.dart b/pkg/compiler/lib/src/js_backend/annotations.dart
index 1879291..048ec24 100644
--- a/pkg/compiler/lib/src/js_backend/annotations.dart
+++ b/pkg/compiler/lib/src/js_backend/annotations.dart
@@ -173,14 +173,23 @@
   };
 }
 
+ir.Library _enclosingLibrary(ir.TreeNode node) {
+  while (true) {
+    if (node is ir.Library) return node;
+    if (node is ir.Member) return node.enclosingLibrary;
+    node = node.parent!;
+  }
+}
+
 EnumSet<PragmaAnnotation> processMemberAnnotations(
     CompilerOptions options,
     DiagnosticReporter reporter,
-    ir.Member member,
+    ir.Annotatable node,
     List<PragmaAnnotationData> pragmaAnnotationData) {
   EnumSet<PragmaAnnotation> annotations = EnumSet<PragmaAnnotation>();
 
-  Uri uri = member.enclosingLibrary.importUri;
+  ir.Library library = _enclosingLibrary(node);
+  Uri uri = library.importUri;
   bool platformAnnotationsAllowed =
       options.testMode || uri.isScheme('dart') || maybeEnableNative(uri);
 
@@ -195,23 +204,23 @@
 
         if (data.hasOptions) {
           reporter.reportErrorMessage(
-              computeSourceSpanFromTreeNode(member),
+              computeSourceSpanFromTreeNode(node),
               MessageKind.GENERIC,
               {'text': "@pragma('$name') annotation does not take options"});
         }
         if (annotation.forFunctionsOnly) {
-          if (member is! ir.Procedure && member is! ir.Constructor) {
+          if (node is! ir.Procedure && node is! ir.Constructor) {
             reporter.reportErrorMessage(
-                computeSourceSpanFromTreeNode(member), MessageKind.GENERIC, {
+                computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
               'text': "@pragma('$name') annotation is only supported "
                   "for methods and constructors."
             });
           }
         }
         if (annotation.forFieldsOnly) {
-          if (member is! ir.Field) {
+          if (node is! ir.Field) {
             reporter.reportErrorMessage(
-                computeSourceSpanFromTreeNode(member), MessageKind.GENERIC, {
+                computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
               'text': "@pragma('$name') annotation is only supported "
                   "for fields."
             });
@@ -219,7 +228,7 @@
         }
         if (annotation.internalOnly && !platformAnnotationsAllowed) {
           reporter.reportErrorMessage(
-              computeSourceSpanFromTreeNode(member),
+              computeSourceSpanFromTreeNode(node),
               MessageKind.GENERIC,
               {'text': "Unrecognized dart2js pragma @pragma('$name')"});
         }
@@ -228,7 +237,7 @@
     }
     if (!found) {
       reporter.reportErrorMessage(
-          computeSourceSpanFromTreeNode(member),
+          computeSourceSpanFromTreeNode(node),
           MessageKind.GENERIC,
           {'text': "Unknown dart2js pragma @pragma('$name')"});
     }
@@ -242,7 +251,7 @@
       for (PragmaAnnotation other in implies) {
         if (annotations.contains(other)) {
           reporter.reportHintMessage(
-              computeSourceSpanFromTreeNode(member), MessageKind.GENERIC, {
+              computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
             'text': "@pragma('dart2js:${annotation.name}') implies "
                 "@pragma('dart2js:${other.name}')."
           });
@@ -255,7 +264,7 @@
         if (annotations.contains(other) &&
             !(reportedExclusions[other]?.contains(annotation) ?? false)) {
           reporter.reportErrorMessage(
-              computeSourceSpanFromTreeNode(member), MessageKind.GENERIC, {
+              computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
             'text': "@pragma('dart2js:${annotation.name}') must not be used "
                 "with @pragma('dart2js:${other.name}')."
           });
@@ -268,7 +277,7 @@
       for (PragmaAnnotation other in requires) {
         if (!annotations.contains(other)) {
           reporter.reportErrorMessage(
-              computeSourceSpanFromTreeNode(member), MessageKind.GENERIC, {
+              computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
             'text': "@pragma('dart2js:${annotation.name}') should always be "
                 "combined with @pragma('dart2js:${other.name}')."
           });
@@ -282,8 +291,9 @@
 abstract class AnnotationsData {
   /// Deserializes a [AnnotationsData] object from [source].
   factory AnnotationsData.readFromDataSource(
-          CompilerOptions options, DataSourceReader source) =
-      AnnotationsDataImpl.readFromDataSource;
+      CompilerOptions options,
+      DiagnosticReporter reporter,
+      DataSourceReader source) = AnnotationsDataImpl.readFromDataSource;
 
   /// Serializes this [AnnotationsData] to [sink].
   void writeToDataSink(DataSinkWriter sink);
@@ -300,6 +310,8 @@
   bool hasTryInline(MemberEntity member);
 
   /// Returns `true` if inlining is disabled at call sites inside [member].
+  // TODO(49475): This should be a property of call site, but ssa/builder.dart
+  // does not currently always pass the call site ir.TreeNode.
   bool hasDisableInlining(MemberEntity member);
 
   /// Returns `true` if [member] has a `@pragma('dart2js:disableFinal')`
@@ -320,11 +332,13 @@
   /// What the compiler should do with parameter type assertions in [member].
   ///
   /// If [member] is `null`, the default policy is returned.
+  // TODO(49475): Need this be nullable?
   CheckPolicy getParameterCheckPolicy(MemberEntity? member);
 
   /// What the compiler should do with implicit downcasts in [member].
   ///
   /// If [member] is `null`, the default policy is returned.
+  // TODO(49475): Need this be nullable?
   CheckPolicy getImplicitDowncastCheckPolicy(MemberEntity? member);
 
   /// What the compiler should do with a boolean value in a condition context
@@ -332,22 +346,28 @@
   /// it to be null.
   ///
   /// If [member] is `null`, the default policy is returned.
+  // TODO(49475): Need this be nullable?
   CheckPolicy getConditionCheckPolicy(MemberEntity? member);
 
   /// What the compiler should do with explicit casts in [member].
   ///
   /// If [member] is `null`, the default policy is returned.
+  // TODO(49475): Need this be nullable?
   CheckPolicy getExplicitCastCheckPolicy(MemberEntity? member);
 
   /// What the compiler should do with index bounds checks `[]`, `[]=` and
   /// `removeLast()` operations in the body of [member].
   ///
   /// If [member] is `null`, the default policy is returned.
+  // TODO(49475): Need this be nullable?
   CheckPolicy getIndexBoundsCheckPolicy(MemberEntity? member);
 
-  /// What the compiler should do with late field checks in the body of
-  /// [member]. [member] is usually the getter or setter for a late field.
-  CheckPolicy getLateVariableCheckPolicy(MemberEntity member);
+  /// What the compiler should do with late field checks at a position in the
+  /// body of a method. The method is usually the getter or setter for a late
+  /// field.
+  // If we change our late field lowering to happen later, [node] could be the
+  // [ir.Field].
+  CheckPolicy getLateVariableCheckPolicyAt(ir.TreeNode node);
 }
 
 class AnnotationsDataImpl implements AnnotationsData {
@@ -355,6 +375,9 @@
   /// debugging data stream.
   static const String tag = 'annotations-data';
 
+  final CompilerOptions _options;
+  final DiagnosticReporter _reporter;
+
   final CheckPolicy _defaultParameterCheckPolicy;
   final CheckPolicy _defaultImplicitDowncastCheckPolicy;
   final CheckPolicy _defaultConditionCheckPolicy;
@@ -362,10 +385,28 @@
   final CheckPolicy _defaultIndexBoundsCheckPolicy;
   final CheckPolicy _defaultLateVariableCheckPolicy;
   final bool _defaultDisableInlining;
+
+  /// Pragma annotations for members. These are captured for the K-entities and
+  /// translated to J-entities.
+  // TODO(49475): Change the queries that use [pragmaAnnotation] to use the
+  // DirectivesContext so that we can avoid the need for persisting annotations.
   final Map<MemberEntity, EnumSet<PragmaAnnotation>> pragmaAnnotations;
 
-  AnnotationsDataImpl(CompilerOptions options, this.pragmaAnnotations)
-      : this._defaultParameterCheckPolicy = options.defaultParameterCheckPolicy,
+  /// Pragma annotation environments for annotatable places in the Kernel
+  /// AST. These annotations generated on demand and not precomputed or
+  /// persisted.  This map is a cache of the pragma annotation environment and
+  /// its enclosing enviroments.
+  // TODO(49475): Periodically clear this map to release references to tree
+  // nodes.
+  final Map<ir.Annotatable, DirectivesContext> _nodeToContextMap = {};
+
+  /// Root annotation environment that allows similar
+  final DirectivesContext _root = DirectivesContext.root();
+
+  AnnotationsDataImpl(
+      CompilerOptions options, this._reporter, this.pragmaAnnotations)
+      : this._options = options,
+        this._defaultParameterCheckPolicy = options.defaultParameterCheckPolicy,
         this._defaultImplicitDowncastCheckPolicy =
             options.defaultImplicitDowncastCheckPolicy,
         this._defaultConditionCheckPolicy = options.defaultConditionCheckPolicy,
@@ -376,14 +417,14 @@
         this._defaultLateVariableCheckPolicy = CheckPolicy.checked,
         this._defaultDisableInlining = options.disableInlining;
 
-  factory AnnotationsDataImpl.readFromDataSource(
-      CompilerOptions options, DataSourceReader source) {
+  factory AnnotationsDataImpl.readFromDataSource(CompilerOptions options,
+      DiagnosticReporter reporter, DataSourceReader source) {
     source.begin(tag);
     Map<MemberEntity, EnumSet<PragmaAnnotation>> pragmaAnnotations =
         source.readMemberMap(
             (MemberEntity member) => EnumSet.fromValue(source.readInt()));
     source.end(tag);
-    return AnnotationsDataImpl(options, pragmaAnnotations);
+    return AnnotationsDataImpl(options, reporter, pragmaAnnotations);
   }
 
   @override
@@ -522,18 +563,33 @@
   }
 
   @override
-  CheckPolicy getLateVariableCheckPolicy(MemberEntity member) {
-    EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
-    if (annotations != null) {
+  CheckPolicy getLateVariableCheckPolicyAt(ir.TreeNode node) {
+    return _getLateVariableCheckPolicyAt(_findContext(node));
+  }
+
+  CheckPolicy _getLateVariableCheckPolicyAt(DirectivesContext? context) {
+    while (context != null) {
+      EnumSet<PragmaAnnotation>? annotations = context.annotations;
       if (annotations.contains(PragmaAnnotation.lateTrust)) {
         return CheckPolicy.trusted;
       } else if (annotations.contains(PragmaAnnotation.lateCheck)) {
         return CheckPolicy.checked;
       }
+      context = context.parent;
     }
-    // TODO(sra): Look for annotations on enclosing class and library.
     return _defaultLateVariableCheckPolicy;
   }
+
+  DirectivesContext _findContext(ir.TreeNode startNode) {
+    ir.TreeNode? node = startNode;
+    while (node is! ir.Annotatable) {
+      if (node == null) return _root;
+      node = node.parent;
+    }
+    return _nodeToContextMap[node] ??= _findContext(node.parent!).extend(
+        processMemberAnnotations(_options, _reporter, node,
+            computePragmaAnnotationDataFromIr(node)));
+  }
 }
 
 class AnnotationsDataBuilder {
@@ -546,7 +602,74 @@
     }
   }
 
-  AnnotationsData close(CompilerOptions options) {
-    return AnnotationsDataImpl(options, pragmaAnnotations);
+  AnnotationsData close(CompilerOptions options, DiagnosticReporter reporter) {
+    return AnnotationsDataImpl(options, reporter, pragmaAnnotations);
+  }
+}
+
+/// A [DirectivesContext] is a chain of enclosing parent annotatable
+/// scopes.
+///
+/// The context chain for a location always starts with a node that reflects the
+/// annotations at the location so that it is possible to determine if an
+/// annotation is 'on' the element. Chains for different locations that have the
+/// same structure are shared. `method1` and `method2` have the same annotations
+/// in scope, with no annotations on the method itself but inheriting
+/// `late:trust` from the class scope. This is represented by DirectivesContext
+/// [D].
+///
+/// Links in the context chain above the element may be compressed, so `class
+/// DD` and `method4` share the chain [F] with no annotations on the element but
+/// inheriting `late:check` from the enclosing library scope.
+///
+///     @pragma('dart2js:late:check')
+///     library foo;  // [B]
+///
+///     @pragma('dart2js:late:trust')
+///     class CC {  // [C]
+///       method1(){}  // [D]
+///       method2(){}  // [D]
+///       @pragma('dart2js:noInline')
+///       method3(){}  // [E]
+///     }
+///
+///     class DD {  // [F]
+///       method4(); // [F]
+///     }
+///
+///
+///     A: parent: null,  pragmas: {}
+///
+///     B: parent: A,     pragmas: {late:check}
+///
+///     C: parent: B,     pragmas: {late:trust}
+///
+///     D: parent: C,     pragmas: {}
+///     E: parent: C,     pragmas: {noInline}
+///
+///     F: parent: B,     pragmas: {}
+///
+/// The root scope [A] is empty. We could remove it and start the root scope at
+/// the library, but the shared root might be a good place to put a set of
+/// annotations derived from the command-line.
+///
+/// If we ever introduce a singled annotation that means something different in
+/// different positions (e.g. on a class vs. on a method), we might want to make
+/// the [DirectivesContext] have a 'scope-kind'.
+class DirectivesContext {
+  final DirectivesContext? parent;
+  final EnumSet<PragmaAnnotation> annotations;
+
+  Map<EnumSet<PragmaAnnotation>, DirectivesContext>? _children;
+
+  DirectivesContext._(this.parent, this.annotations);
+
+  DirectivesContext.root() : this._(null, EnumSet<PragmaAnnotation>());
+
+  DirectivesContext extend(EnumSet<PragmaAnnotation> annotations) {
+    // Shorten chains of equivalent sets of annotations.
+    if (this.annotations == annotations) return this;
+    final children = _children ??= {};
+    return children[annotations] ??= DirectivesContext._(this, annotations);
   }
 }
diff --git a/pkg/compiler/lib/src/js_model/js_strategy.dart b/pkg/compiler/lib/src/js_model/js_strategy.dart
index 54a43e6..3353396 100644
--- a/pkg/compiler/lib/src/js_model/js_strategy.dart
+++ b/pkg/compiler/lib/src/js_model/js_strategy.dart
@@ -172,8 +172,12 @@
         closedWorld.annotationsData);
     ClosureDataBuilder closureDataBuilder = ClosureDataBuilder(
         _compiler.reporter, _elementMap, closedWorld.annotationsData);
-    JsClosedWorldBuilder closedWorldBuilder = JsClosedWorldBuilder(_elementMap,
-        closureDataBuilder, _compiler.options, _compiler.abstractValueStrategy);
+    JsClosedWorldBuilder closedWorldBuilder = JsClosedWorldBuilder(
+        _elementMap,
+        closureDataBuilder,
+        _compiler.options,
+        _compiler.reporter,
+        _compiler.abstractValueStrategy);
     JClosedWorld jClosedWorld = closedWorldBuilder.convertClosedWorld(
         closedWorld, strategy.closureModels, outputUnitData);
     _elementMap.lateOutputUnitDataBuilder =
diff --git a/pkg/compiler/lib/src/js_model/js_world.dart b/pkg/compiler/lib/src/js_model/js_world.dart
index 6d039aa..1d405d4 100644
--- a/pkg/compiler/lib/src/js_model/js_world.dart
+++ b/pkg/compiler/lib/src/js_model/js_world.dart
@@ -163,7 +163,7 @@
         source.readClassMap(() => source.readClasses().toSet());
 
     AnnotationsData annotationsData =
-        AnnotationsData.readFromDataSource(options, source);
+        AnnotationsData.readFromDataSource(options, reporter, source);
 
     ClosureData closureData =
         ClosureData.readFromDataSource(elementMap, source);
diff --git a/pkg/compiler/lib/src/js_model/js_world_builder.dart b/pkg/compiler/lib/src/js_model/js_world_builder.dart
index 7957441..673cf82 100644
--- a/pkg/compiler/lib/src/js_model/js_world_builder.dart
+++ b/pkg/compiler/lib/src/js_model/js_world_builder.dart
@@ -46,10 +46,11 @@
   final Map<ClassEntity, ClassSet> _classSets = <ClassEntity, ClassSet>{};
   final ClosureDataBuilder _closureDataBuilder;
   final CompilerOptions _options;
+  final DiagnosticReporter _reporter;
   final AbstractValueStrategy _abstractValueStrategy;
 
   JsClosedWorldBuilder(this._elementMap, this._closureDataBuilder,
-      this._options, this._abstractValueStrategy);
+      this._options, this._reporter, this._abstractValueStrategy);
 
   ElementEnvironment get _elementEnvironment => _elementMap.elementEnvironment;
   CommonElements get _commonElements => _elementMap.commonElements;
@@ -200,7 +201,7 @@
         JFieldAnalysis.from(closedWorld, map, _options);
 
     AnnotationsDataImpl oldAnnotationsData = closedWorld.annotationsData;
-    AnnotationsData annotationsData = AnnotationsDataImpl(_options,
+    AnnotationsData annotationsData = AnnotationsDataImpl(_options, _reporter,
         map.toBackendMemberMap(oldAnnotationsData.pragmaAnnotations, identity));
 
     OutputUnitData outputUnitData =
diff --git a/pkg/compiler/lib/src/kernel/kernel_strategy.dart b/pkg/compiler/lib/src/kernel/kernel_strategy.dart
index 80486a8..922b7da 100644
--- a/pkg/compiler/lib/src/kernel/kernel_strategy.dart
+++ b/pkg/compiler/lib/src/kernel/kernel_strategy.dart
@@ -154,8 +154,8 @@
     // TODO(johnniwinther): This is a hack. The annotation data is built while
     // using it. With CFE constants the annotations data can be built fully
     // before creating the resolution enqueuer.
-    AnnotationsData annotationsData = AnnotationsDataImpl(
-        compiler.options, annotationsDataBuilder.pragmaAnnotations);
+    AnnotationsData annotationsData = AnnotationsDataImpl(compiler.options,
+        compiler.reporter, annotationsDataBuilder.pragmaAnnotations);
     InterceptorDataBuilder interceptorDataBuilder = InterceptorDataBuilderImpl(
         nativeBasicData, elementEnvironment, commonElements);
     return ResolutionEnqueuer(
@@ -392,6 +392,9 @@
           node,
           pragmaAnnotationData);
       _annotationsDataBuilder.registerPragmaAnnotations(element, annotations);
+      // TODO(sra): Replace the above three statements with a single call to a
+      // new API on AnnotationsData that causes the annotations to be parsed and
+      // checked.
 
       ModularMemberData modularMemberData =
           _modularStrategy.getModularMemberData(node, annotations);
diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart
index 67b995a..3aa7df2 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -4958,8 +4958,8 @@
     HInstruction value = arguments[0];
     HInstruction name = options.omitLateNames ? null : arguments[1];
 
-    CheckPolicy policy = closedWorld.annotationsData
-        .getLateVariableCheckPolicy(_currentFrame.member);
+    CheckPolicy policy =
+        closedWorld.annotationsData.getLateVariableCheckPolicyAt(invocation);
 
     push(HLateReadCheck(value, name, policy.isTrusted,
         _abstractValueDomain.excludeLateSentinel(value.instructionType))
@@ -4983,8 +4983,8 @@
     HInstruction value = arguments[0];
     HInstruction name = options.omitLateNames ? null : arguments[1];
 
-    CheckPolicy policy = closedWorld.annotationsData
-        .getLateVariableCheckPolicy(_currentFrame.member);
+    CheckPolicy policy =
+        closedWorld.annotationsData.getLateVariableCheckPolicyAt(invocation);
 
     push(HLateWriteOnceCheck(
         value, name, policy.isTrusted, _abstractValueDomain.dynamicType)
@@ -5008,8 +5008,8 @@
     HInstruction value = arguments[0];
     HInstruction name = options.omitLateNames ? null : arguments[1];
 
-    CheckPolicy policy = closedWorld.annotationsData
-        .getLateVariableCheckPolicy(_currentFrame.member);
+    CheckPolicy policy =
+        closedWorld.annotationsData.getLateVariableCheckPolicyAt(invocation);
 
     push(HLateInitializeOnceCheck(
         value, name, policy.isTrusted, _abstractValueDomain.dynamicType)
diff --git a/pkg/compiler/lib/src/universe/resolution_world_builder.dart b/pkg/compiler/lib/src/universe/resolution_world_builder.dart
index a49196d..40433ca 100644
--- a/pkg/compiler/lib/src/universe/resolution_world_builder.dart
+++ b/pkg/compiler/lib/src/universe/resolution_world_builder.dart
@@ -1002,7 +1002,7 @@
         mixinUses: _classHierarchyBuilder.mixinUses,
         typesImplementedBySubclasses: typesImplementedBySubclasses,
         classHierarchy: _classHierarchyBuilder.close(),
-        annotationsData: _annotationsDataBuilder.close(_options),
+        annotationsData: _annotationsDataBuilder.close(_options, reporter),
         isChecks: _isChecks,
         staticTypeArgumentDependencies: staticTypeArgumentDependencies,
         dynamicTypeArgumentDependencies: dynamicTypeArgumentDependencies,
diff --git a/tests/web/late_field_checks_common.dart b/tests/web/late_field_checks_common.dart
new file mode 100644
index 0000000..e0e380c
--- /dev/null
+++ b/tests/web/late_field_checks_common.dart
@@ -0,0 +1,106 @@
+// Copyright (c) 2022, 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.
+
+library late_field_checks.common;
+
+import 'package:expect/expect.dart';
+
+// True for DDC and VM.
+final bool isAlwaysChecked = !const bool.fromEnvironment('dart2js');
+
+/// Interface for test object classes that have a field. All the test objects
+/// implement this interface.
+abstract class Field {
+  abstract int field;
+}
+
+/// Tag interface for a class where the `late` field is also `final`.
+class Final {}
+
+/// Tag interface for a class where late field is checked.
+class Checked {}
+
+/// Tag interface for a class where the late field checks are not performed for
+/// the late field.
+class Trusted {}
+
+/// The library 'name' e.g. 'LibraryCheck'
+/// Nullable so it can be cleared to ensure the library's main() sets it.
+String? libraryName;
+
+String describeClass(Field object) {
+  final name = '${libraryName!}.${object.runtimeType}';
+  final interfaces = [
+    if (object is Final) 'Final',
+    'Field',
+    if (object is Checked) 'Checked',
+    if (object is Trusted) 'Trusted'
+  ];
+
+  return '$name implements ${interfaces.join(", ")}';
+}
+
+void test(Field Function() factory) {
+  // Consistency checks.
+  final o1 = factory();
+  // Get the class description to ensure library name is set.
+  final description = describeClass(o1);
+  print('-- $description');
+  Expect.isTrue(o1 is Checked || o1 is Trusted,
+      'Test class must implement one of Checked or Trusted: $description');
+  Expect.isFalse(o1 is Checked && o1 is Trusted,
+      'Test class must not implement both of Checked or Trusted: $description');
+
+  // Setter then Getter should not throw.
+  final o2 = factory();
+  o2.field = 100;
+  Expect.equals(100, o2.field);
+
+  testGetterBeforeSetter(factory());
+  testDoubleSetter(factory());
+}
+
+void testGetterBeforeSetter(Field object) {
+  final isChecked = isAlwaysChecked || object is Checked;
+
+  bool threw = false;
+  try {
+    _sink = object.field;
+  } catch (e, s) {
+    threw = true;
+  }
+
+  if (threw == isChecked) return;
+  _fail(object, threw, 'getter before setter');
+}
+
+void testDoubleSetter(Field object) {
+  final isChecked = isAlwaysChecked || object is Checked;
+
+  object.field = 101;
+  bool threw = false;
+  try {
+    object.field = 102;
+  } catch (e, s) {
+    threw = true;
+  }
+
+  if (object is Final) {
+    if (threw == isChecked) return;
+    _fail(object, threw, 'double setter');
+  }
+
+  Expect.equals(102, object.field);
+}
+
+int _sink = 0;
+
+void _fail(Field object, bool threw, String testDescription) {
+  final classDescription = describeClass(object);
+  if (threw) {
+    Expect.fail('Should not throw for $testDescription: $classDescription');
+  } else {
+    Expect.fail('Failed to throw for $testDescription: $classDescription');
+  }
+}
diff --git a/tests/web/late_field_checks_lib_check.dart b/tests/web/late_field_checks_lib_check.dart
new file mode 100644
index 0000000..0cd85ea
--- /dev/null
+++ b/tests/web/late_field_checks_lib_check.dart
@@ -0,0 +1,129 @@
+// Copyright (c) 2022, 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.
+
+@pragma('dart2js:late:check')
+library late_field_checks.lib_check;
+
+import 'late_field_checks_common.dart';
+
+void main() {
+  libraryName = 'LibraryCheck';
+
+  test(() => ClassNoneFieldNone());
+  test(() => ClassNoneFinalFieldNone());
+  test(() => ClassNoneFieldTrust());
+  test(() => ClassNoneFinalFieldTrust());
+  test(() => ClassNoneFieldCheck());
+  test(() => ClassNoneFinalFieldCheck());
+
+  test(() => ClassTrustFieldNone());
+  test(() => ClassTrustFinalFieldNone());
+  test(() => ClassTrustFieldTrust());
+  test(() => ClassTrustFinalFieldTrust());
+  test(() => ClassTrustFieldCheck());
+  test(() => ClassTrustFinalFieldCheck());
+
+  test(() => ClassCheckFieldNone());
+  test(() => ClassCheckFinalFieldNone());
+  test(() => ClassCheckFieldTrust());
+  test(() => ClassCheckFinalFieldTrust());
+  test(() => ClassCheckFieldCheck());
+  test(() => ClassCheckFinalFieldCheck());
+}
+
+class ClassNoneFieldNone implements Field, Checked {
+  late int field;
+}
+
+class ClassNoneFinalFieldNone implements Field, Final, Checked {
+  late final int field;
+}
+
+class ClassNoneFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+class ClassNoneFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+class ClassNoneFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+class ClassNoneFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldNone implements Field, Trusted {
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldNone implements Field, Final, Trusted {
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldNone implements Field, Checked {
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldNone implements Field, Final, Checked {
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
diff --git a/tests/web/late_field_checks_lib_none.dart b/tests/web/late_field_checks_lib_none.dart
new file mode 100644
index 0000000..d39d61f
--- /dev/null
+++ b/tests/web/late_field_checks_lib_none.dart
@@ -0,0 +1,129 @@
+// Copyright (c) 2022, 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.
+
+// No annotation here.
+library late_field_checks.lib_none;
+
+import 'late_field_checks_common.dart';
+
+void main() {
+  libraryName = 'LibraryNone';
+
+  test(() => ClassNoneFieldNone());
+  test(() => ClassNoneFinalFieldNone());
+  test(() => ClassNoneFieldTrust());
+  test(() => ClassNoneFinalFieldTrust());
+  test(() => ClassNoneFieldCheck());
+  test(() => ClassNoneFinalFieldCheck());
+
+  test(() => ClassTrustFieldNone());
+  test(() => ClassTrustFinalFieldNone());
+  test(() => ClassTrustFieldTrust());
+  test(() => ClassTrustFinalFieldTrust());
+  test(() => ClassTrustFieldCheck());
+  test(() => ClassTrustFinalFieldCheck());
+
+  test(() => ClassCheckFieldNone());
+  test(() => ClassCheckFinalFieldNone());
+  test(() => ClassCheckFieldTrust());
+  test(() => ClassCheckFinalFieldTrust());
+  test(() => ClassCheckFieldCheck());
+  test(() => ClassCheckFinalFieldCheck());
+}
+
+class ClassNoneFieldNone implements Field, Checked {
+  late int field;
+}
+
+class ClassNoneFinalFieldNone implements Field, Final, Checked {
+  late final int field;
+}
+
+class ClassNoneFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+class ClassNoneFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+class ClassNoneFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+class ClassNoneFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldNone implements Field, Trusted {
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldNone implements Field, Final, Trusted {
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldNone implements Field, Checked {
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldNone implements Field, Final, Checked {
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
diff --git a/tests/web/late_field_checks_lib_trust.dart b/tests/web/late_field_checks_lib_trust.dart
new file mode 100644
index 0000000..13c19c3
--- /dev/null
+++ b/tests/web/late_field_checks_lib_trust.dart
@@ -0,0 +1,129 @@
+// Copyright (c) 2022, 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.
+
+@pragma('dart2js:late:trust')
+library late_field_checks.lib_trust;
+
+import 'late_field_checks_common.dart';
+
+void main() {
+  libraryName = 'LibraryTrust';
+
+  test(() => ClassNoneFieldNone());
+  test(() => ClassNoneFinalFieldNone());
+  test(() => ClassNoneFieldTrust());
+  test(() => ClassNoneFinalFieldTrust());
+  test(() => ClassNoneFieldCheck());
+  test(() => ClassNoneFinalFieldCheck());
+
+  test(() => ClassTrustFieldNone());
+  test(() => ClassTrustFinalFieldNone());
+  test(() => ClassTrustFieldTrust());
+  test(() => ClassTrustFinalFieldTrust());
+  test(() => ClassTrustFieldCheck());
+  test(() => ClassTrustFinalFieldCheck());
+
+  test(() => ClassCheckFieldNone());
+  test(() => ClassCheckFinalFieldNone());
+  test(() => ClassCheckFieldTrust());
+  test(() => ClassCheckFinalFieldTrust());
+  test(() => ClassCheckFieldCheck());
+  test(() => ClassCheckFinalFieldCheck());
+}
+
+class ClassNoneFieldNone implements Field, Trusted {
+  late int field;
+}
+
+class ClassNoneFinalFieldNone implements Field, Final, Trusted {
+  late final int field;
+}
+
+class ClassNoneFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+class ClassNoneFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+class ClassNoneFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+class ClassNoneFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldNone implements Field, Trusted {
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldNone implements Field, Final, Trusted {
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:trust')
+class ClassTrustFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldNone implements Field, Checked {
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldNone implements Field, Final, Checked {
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldTrust implements Field, Trusted {
+  @pragma('dart2js:late:trust')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldTrust implements Field, Final, Trusted {
+  @pragma('dart2js:late:trust')
+  late final int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFieldCheck implements Field, Checked {
+  @pragma('dart2js:late:check')
+  late int field;
+}
+
+@pragma('dart2js:late:check')
+class ClassCheckFinalFieldCheck implements Field, Final, Checked {
+  @pragma('dart2js:late:check')
+  late final int field;
+}
diff --git a/tests/web/late_field_checks_test.dart b/tests/web/late_field_checks_test.dart
new file mode 100644
index 0000000..f8777e1
--- /dev/null
+++ b/tests/web/late_field_checks_test.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2022, 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.
+
+// dart2jsOptions=-Ddart2js=true
+
+// Behavioral test for annotations that control checking of late fields.
+// See `late_field_checks_common.dart` for details.
+
+import 'package:expect/expect.dart';
+
+import 'late_field_checks_common.dart' show libraryName;
+import 'late_field_checks_lib_none.dart' as libNone;
+import 'late_field_checks_lib_trust.dart' as libTrust;
+import 'late_field_checks_lib_check.dart' as libCheck;
+
+void main() {
+  libraryName = null;
+  libNone.main();
+  Expect.equals('LibraryNone', libraryName);
+
+  libraryName = null;
+  libCheck.main();
+  Expect.equals('LibraryCheck', libraryName);
+
+  libraryName = null;
+  libTrust.main();
+  Expect.equals('LibraryTrust', libraryName);
+}