linter: Allow lint rules to specify afterLibrary actions

Add `NodeLintRegistry.afterLibrary` API which allows a lint rule to
register a callback to be called after the last CompilationUnit is
visited.

Change-Id: I77bf279412617e6f2c825b6dbeaf34849e515156
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/368524
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart b/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
index e13a22a..fdb0b87 100644
--- a/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
@@ -51,6 +51,8 @@
 import 'package:analyzer/src/util/performance/operation_performance.dart';
 import 'package:analyzer/src/utilities/extensions/version.dart';
 import 'package:analyzer/src/workspace/pub.dart';
+import 'package:analyzer/src/workspace/workspace.dart';
+import 'package:collection/collection.dart';
 
 class AnalysisForCompletionResult {
   final FileState fileState;
@@ -318,19 +320,7 @@
     }
 
     if (_analysisOptions.lint) {
-      var analysesToContextUnits = {
-        for (var unitAnalysis in _libraryUnits.values)
-          unitAnalysis: LinterContextUnit(
-            unitAnalysis.file.content,
-            unitAnalysis.unit,
-            unitAnalysis.errorReporter,
-          ),
-      };
-      var allUnits = analysesToContextUnits.values.toList();
-      for (var MapEntry(key: unitAnalysis, value: currentUnit)
-          in analysesToContextUnits.entries) {
-        _computeLints(unitAnalysis, allUnits, currentUnit);
-      }
+      _computeLints();
     }
 
     _checkForInconsistentLanguageVersionOverride();
@@ -342,52 +332,77 @@
         unitAnalysis.errorReporter,
         unitAnalysis.errorListener.errors,
         unitAnalysis.ignoreInfo,
-        unitAnalysis.lineInfo,
+        unitAnalysis.unit.lineInfo,
         _analysisOptions.unignorableNames,
       ).reportErrors();
     }
   }
 
-  void _computeLints(
-    UnitAnalysis unitAnalysis,
-    List<LinterContextUnit> allUnits,
-    LinterContextUnit currentUnit,
-  ) {
-    // Skip computing lints on macro generated augmentations.
-    // See: https://github.com/dart-lang/sdk/issues/54875
-    if (unitAnalysis.file.isMacroAugmentation) return;
+  void _computeLints() {
+    var definingUnit = _libraryElement.definingCompilationUnit;
+    var analysesToContextUnits = <UnitAnalysis, LinterContextUnit>{};
+    LinterContextUnit? definingContextUnit;
+    WorkspacePackage? workspacePackage;
+    for (var unitAnalysis in _libraryUnits.values) {
+      var linterContextUnit = LinterContextUnit(
+        unitAnalysis.file.content,
+        unitAnalysis.unit,
+        unitAnalysis.errorReporter,
+      );
+      analysesToContextUnits[unitAnalysis] = linterContextUnit;
+      if (unitAnalysis.unit.declaredElement == definingUnit) {
+        definingContextUnit = linterContextUnit;
+        workspacePackage = unitAnalysis.file.workspacePackage;
+      }
+    }
 
-    var unit = currentUnit.unit;
-    var errorReporter = currentUnit.errorReporter;
+    var allUnits = analysesToContextUnits.values.toList();
+    definingContextUnit ??= allUnits.first;
 
     var enableTiming = _analysisOptions.enableTiming;
     var nodeRegistry = NodeLintRegistry(enableTiming);
-
     var context = LinterContextImpl(
       allUnits,
-      currentUnit,
+      definingContextUnit,
       _typeProvider,
       _typeSystem,
       _inheritance,
-      unitAnalysis.file.workspacePackage,
+      workspacePackage,
     );
+
     for (var linter in _analysisOptions.lintRules) {
-      linter.reporter = errorReporter;
       var timer = enableTiming ? lintRuleTimers.getTimer(linter) : null;
       timer?.start();
       linter.registerNodeProcessors(nodeRegistry, context);
       timer?.stop();
     }
 
-    // Run lints that handle specific node types.
-    unit.accept(
-      LinterVisitor(
-        nodeRegistry,
-        LinterExceptionHandler(
-          propagateExceptions: _analysisOptions.propagateLinterExceptions,
-        ).logException,
-      ),
-    );
+    var logException = LinterExceptionHandler(
+      propagateExceptions: _analysisOptions.propagateLinterExceptions,
+    ).logException;
+
+    for (var MapEntry(key: unitAnalysis, value: currentUnit)
+        in analysesToContextUnits.entries) {
+      // Skip computing lints on macro generated augmentations.
+      // See: https://github.com/dart-lang/sdk/issues/54875
+      if (unitAnalysis.file.isMacroAugmentation) return;
+
+      var unit = currentUnit.unit;
+      var errorReporter = currentUnit.errorReporter;
+
+      for (var linter in _analysisOptions.lintRules) {
+        linter.reporter = errorReporter;
+      }
+
+      // Run lint rules that handle specific node types.
+      unit.accept(
+        LinterVisitor(nodeRegistry, logException),
+      );
+    }
+
+    // Now that all lint rules have visited the code in each of the compilation
+    // units, we can accept each lint rule's `afterLibrary` hook.
+    LinterVisitor(nodeRegistry, logException).afterLibrary();
   }
 
   void _computeVerifyErrors(UnitAnalysis unitAnalysis) {
@@ -567,10 +582,7 @@
     var result = UnitAnalysis(
       file: file,
       errorListener: errorListener,
-      errorReporter: ErrorReporter(errorListener, file.source),
       unit: unit,
-      lineInfo: unit.lineInfo,
-      ignoreInfo: IgnoreInfo.forDart(unit, file.content),
     );
     _libraryUnits[file] = result;
     return result;
diff --git a/pkg/analyzer/lib/src/dart/analysis/unit_analysis.dart b/pkg/analyzer/lib/src/dart/analysis/unit_analysis.dart
index 3470580..a7a0636 100644
--- a/pkg/analyzer/lib/src/dart/analysis/unit_analysis.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/unit_analysis.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/error/listener.dart';
-import 'package:analyzer/source/line_info.dart';
 import 'package:analyzer/src/dart/analysis/file_state.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
 import 'package:analyzer/src/dart/element/element.dart';
@@ -15,7 +14,6 @@
   final RecordingErrorListener errorListener;
   final ErrorReporter errorReporter;
   final CompilationUnitImpl unit;
-  final LineInfo lineInfo;
   final IgnoreInfo ignoreInfo;
 
   late final CompilationUnitElementImpl element;
@@ -23,9 +21,7 @@
   UnitAnalysis({
     required this.file,
     required this.errorListener,
-    required this.errorReporter,
     required this.unit,
-    required this.lineInfo,
-    required this.ignoreInfo,
-  });
+  })  : errorReporter = ErrorReporter(errorListener, file.source),
+        ignoreInfo = IgnoreInfo.forDart(unit, file.content);
 }
diff --git a/pkg/analyzer/lib/src/lint/linter.dart b/pkg/analyzer/lib/src/lint/linter.dart
index 0a4fb8f5..35c9c7b 100644
--- a/pkg/analyzer/lib/src/lint/linter.dart
+++ b/pkg/analyzer/lib/src/lint/linter.dart
@@ -4,7 +4,6 @@
 
 import 'dart:io';
 
-import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/constant/value.dart';
 import 'package:analyzer/dart/element/element.dart';
@@ -184,18 +183,27 @@
 abstract class LinterContext {
   List<LinterContextUnit> get allUnits;
 
-  LinterContextUnit get currentUnit;
+  LinterContextUnit get definingUnit;
 
   InheritanceManager3 get inheritanceManager;
 
+  /// Whether the [definingUnit] is in a package's top-level `lib` directory.
+  bool get isInLibDir;
+
+  LibraryElement? get libraryElement;
+
   WorkspacePackage? get package;
 
   TypeProvider get typeProvider;
 
   TypeSystem get typeSystem;
 
-  /// Returns whether the [feature] is enabled in the library being linted.
-  bool isEnabled(Feature feature);
+  static bool _isInLibDir(String? path, WorkspacePackage? package) {
+    if (package == null) return false;
+    if (path == null) return false;
+    var libDir = p.join(package.root, 'lib');
+    return p.isWithin(libDir, path);
+  }
 }
 
 class LinterContextImpl implements LinterContext {
@@ -203,7 +211,7 @@
   final List<LinterContextUnit> allUnits;
 
   @override
-  final LinterContextUnit currentUnit;
+  final LinterContextUnit definingUnit;
 
   @override
   final WorkspacePackage? package;
@@ -218,7 +226,7 @@
 
   LinterContextImpl(
     this.allUnits,
-    this.currentUnit,
+    this.definingUnit,
     this.typeProvider,
     this.typeSystem,
     this.inheritanceManager,
@@ -226,10 +234,12 @@
   );
 
   @override
-  bool isEnabled(Feature feature) {
-    var unitElement = currentUnit.unit.declaredElement!;
-    return unitElement.library.featureSet.isEnabled(feature);
-  }
+  bool get isInLibDir => LinterContext._isInLibDir(
+      definingUnit.unit.declaredElement?.source.fullName, package);
+
+  @override
+  LibraryElement get libraryElement =>
+      definingUnit.unit.declaredElement!.library;
 }
 
 class LinterContextParsedImpl implements LinterContext {
@@ -237,7 +247,7 @@
   final List<LinterContextUnit> allUnits;
 
   @override
-  final LinterContextUnit currentUnit;
+  final LinterContextUnit definingUnit;
 
   @override
   final WorkspacePackage? package = null;
@@ -247,20 +257,26 @@
 
   LinterContextParsedImpl(
     this.allUnits,
-    this.currentUnit,
+    this.definingUnit,
   );
 
   @override
+  bool get isInLibDir {
+    return LinterContext._isInLibDir(
+        definingUnit.unit.declaredElement?.source.fullName, package);
+  }
+
+  @override
+  LibraryElement get libraryElement =>
+      throw UnsupportedError('LinterContext with parsed results');
+
+  @override
   TypeProvider get typeProvider =>
       throw UnsupportedError('LinterContext with parsed results');
 
   @override
   TypeSystem get typeSystem =>
       throw UnsupportedError('LinterContext with parsed results');
-
-  @override
-  bool isEnabled(Feature feature) =>
-      throw UnsupportedError('LinterContext with parsed results');
 }
 
 class LinterContextUnit {
diff --git a/pkg/analyzer/lib/src/lint/linter_visitor.dart b/pkg/analyzer/lib/src/lint/linter_visitor.dart
index d8a65dd..afa4892 100644
--- a/pkg/analyzer/lib/src/lint/linter_visitor.dart
+++ b/pkg/analyzer/lib/src/lint/linter_visitor.dart
@@ -23,6 +23,10 @@
       : exceptionHandler = exceptionHandler ??
             LinterExceptionHandler(propagateExceptions: true).logException;
 
+  void afterLibrary() {
+    _runAfterLibrarySubscriptions(registry._afterLibrary);
+  }
+
   @override
   void visitAdjacentStrings(AdjacentStrings node) {
     _runSubscriptions(node, registry._forAdjacentStrings);
@@ -1073,10 +1077,19 @@
     node.visitChildren(this);
   }
 
+  void _runAfterLibrarySubscriptions(
+      List<_AfterLibrarySubscription> subscriptions) {
+    for (var subscription in subscriptions) {
+      var timer = subscription.timer;
+      timer?.start();
+      subscription.callback();
+      timer?.stop();
+    }
+  }
+
   void _runSubscriptions<T extends AstNode>(
       T node, List<_Subscription<T>> subscriptions) {
-    for (int i = 0; i < subscriptions.length; i++) {
-      var subscription = subscriptions[i];
+    for (var subscription in subscriptions) {
       var timer = subscription.timer;
       timer?.start();
       try {
@@ -1095,6 +1108,7 @@
 /// The container to register visitors for separate AST node types.
 class NodeLintRegistry {
   final bool enableTiming;
+  final List<_AfterLibrarySubscription> _afterLibrary = [];
   final List<_Subscription<AdjacentStrings>> _forAdjacentStrings = [];
   final List<_Subscription<Annotation>> _forAnnotation = [];
   final List<_Subscription<ArgumentList>> _forArgumentList = [];
@@ -2067,6 +2081,11 @@
     _forYieldStatement.add(_Subscription(linter, visitor, _getTimer(linter)));
   }
 
+  void afterLibrary(LintRule linter, void Function() callback) {
+    _afterLibrary
+        .add(_AfterLibrarySubscription(linter, callback, _getTimer(linter)));
+  }
+
   /// Get the timer associated with the given [linter].
   Stopwatch? _getTimer(LintRule linter) {
     if (enableTiming) {
@@ -2077,6 +2096,14 @@
   }
 }
 
+class _AfterLibrarySubscription {
+  final LintRule linter;
+  final void Function() callback;
+  final Stopwatch? timer;
+
+  _AfterLibrarySubscription(this.linter, this.callback, this.timer);
+}
+
 /// A single subscription for a node type, by the specified [linter].
 class _Subscription<T> {
   final LintRule linter;
diff --git a/pkg/linter/lib/src/rules/always_use_package_imports.dart b/pkg/linter/lib/src/rules/always_use_package_imports.dart
index b802b33..6b2886a 100644
--- a/pkg/linter/lib/src/rules/always_use_package_imports.dart
+++ b/pkg/linter/lib/src/rules/always_use_package_imports.dart
@@ -6,7 +6,6 @@
 import 'package:analyzer/dart/ast/visitor.dart';
 
 import '../analyzer.dart';
-import '../ast.dart';
 
 const _desc = r'Avoid relative imports for files in `lib/`.';
 
@@ -68,10 +67,8 @@
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
     // Relative paths from outside of the lib folder are handled by the
-    // `avoid_relative_lib_imports` lint.
-    if (!isInLibDir(context.currentUnit.unit, context.package)) {
-      return;
-    }
+    // `avoid_relative_lib_imports` lint rule.
+    if (!context.isInLibDir) return;
 
     var visitor = _Visitor(this);
     registry.addImportDirective(this, visitor);
diff --git a/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart b/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart
index b11677f..ef169d1 100644
--- a/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart
+++ b/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart
@@ -9,7 +9,6 @@
 import 'package:analyzer/dart/element/element.dart';
 
 import '../analyzer.dart';
-import '../ast.dart';
 import '../extensions.dart';
 
 const _desc = r"Don't rename parameters of overridden methods.";
@@ -66,9 +65,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!isInLibDir(context.currentUnit.unit, context.package)) {
-      return;
-    }
+    if (!context.isInLibDir) return;
 
     var visitor = _Visitor(this);
     registry.addMethodDeclaration(this, visitor);
diff --git a/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart b/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart
index 65c94c2..ab745f5 100644
--- a/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart
+++ b/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart
@@ -98,7 +98,7 @@
       if (hasFlutter == null) {
         // Clear the previous cache.
         clearCache();
-        var pubspecFile = locatePubspecFile(context.currentUnit.unit);
+        var pubspecFile = locatePubspecFile(context.definingUnit.unit);
         hasFlutter = hasFlutterDep(pubspecFile);
         _rootHasFlutterCache[root] = hasFlutter;
       }
diff --git a/pkg/linter/lib/src/rules/document_ignores.dart b/pkg/linter/lib/src/rules/document_ignores.dart
index 279c195..4450f1e 100644
--- a/pkg/linter/lib/src/rules/document_ignores.dart
+++ b/pkg/linter/lib/src/rules/document_ignores.dart
@@ -52,7 +52,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    var visitor = _Visitor(this, context);
+    var visitor = _Visitor(this);
     registry.addCompilationUnit(this, visitor);
   }
 }
@@ -60,13 +60,11 @@
 class _Visitor extends SimpleAstVisitor<void> {
   final LintRule rule;
 
-  final String content;
-
-  _Visitor(this.rule, LinterContext context)
-      : content = context.currentUnit.content;
+  _Visitor(this.rule);
 
   @override
   void visitCompilationUnit(CompilationUnit node) {
+    var content = node.declaredElement?.source.contents.data;
     for (var comment in node.ignoreComments) {
       var ignoredElements = comment.ignoredElements;
       if (ignoredElements.isEmpty) {
@@ -84,7 +82,8 @@
         // first line.
         var previousLineOffset =
             node.lineInfo.getOffsetOfLine(ignoreCommentLine - 2);
-        if (_startsWithEndOfLineComment(previousLineOffset)) {
+        if (content != null &&
+            _startsWithEndOfLineComment(content, previousLineOffset)) {
           // A preceding comment, which may be attached to a different token,
           // documents this/these ignore(s). For example in:
           //
@@ -102,7 +101,7 @@
 
   /// Returns whether [content] at [offset_] represents starts with optional
   /// whitespace and then an end-of-line comment (two slashes).
-  bool _startsWithEndOfLineComment(int offset_) {
+  bool _startsWithEndOfLineComment(String content, int offset_) {
     var offset = offset_;
     var length = content.length;
     while (offset < length) {
diff --git a/pkg/linter/lib/src/rules/eol_at_end_of_file.dart b/pkg/linter/lib/src/rules/eol_at_end_of_file.dart
index 36180ef..4eddee0 100644
--- a/pkg/linter/lib/src/rules/eol_at_end_of_file.dart
+++ b/pkg/linter/lib/src/rules/eol_at_end_of_file.dart
@@ -57,8 +57,11 @@
 
   @override
   void visitCompilationUnit(CompilationUnit node) {
-    var content = context.currentUnit.content;
-    if (content.isNotEmpty &&
+    var content = node.declaredElement?.source.contents.data;
+    if (content != null &&
+        content.isNotEmpty &&
+        // TODO(srawlins): Re-implement this check without iterating over
+        // various lists of strings.
         (!content.endsWithNewline || content.endsWithMultipleNewlines)) {
       rule.reportLintForOffset(content.trimRight().length, 1);
     }
diff --git a/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart b/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart
index e429da3..196b45f 100644
--- a/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart
+++ b/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart
@@ -195,8 +195,10 @@
         end = lineInfo.getOffsetOfLine(i + 1) - 1;
         var length = end - start;
         if (length > 80) {
-          if (context.currentUnit.content[end] == _lf &&
-              context.currentUnit.content[end - 1] == _cr) {
+          var content = node.declaredElement?.source.contents.data;
+          if (content != null &&
+              content[end] == _lf &&
+              content[end - 1] == _cr) {
             end--;
           }
         }
diff --git a/pkg/linter/lib/src/rules/prefer_relative_imports.dart b/pkg/linter/lib/src/rules/prefer_relative_imports.dart
index d6e59bd..36b0663 100644
--- a/pkg/linter/lib/src/rules/prefer_relative_imports.dart
+++ b/pkg/linter/lib/src/rules/prefer_relative_imports.dart
@@ -8,7 +8,6 @@
 import 'package:path/path.dart' as path;
 
 import '../analyzer.dart';
-import '../ast.dart';
 import 'implementation_imports.dart' show samePackage;
 
 const _desc = r'Prefer relative imports for files in `lib/`.';
@@ -54,9 +53,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!isInLibDir(context.currentUnit.unit, context.package)) {
-      return;
-    }
+    if (!context.isInLibDir) return;
 
     var visitor = _Visitor(this, context);
     registry.addImportDirective(this, visitor);
diff --git a/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart b/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart
index 07ae793..6c15bb4 100644
--- a/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart
+++ b/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart
@@ -79,7 +79,7 @@
         for (var dep in dependencies)
           if (dep.name?.text != null) dep.name!.text!,
       if (devDependencies != null &&
-          !isInPublicDir(context.currentUnit.unit, context.package))
+          !isInPublicDir(context.definingUnit.unit, context.package))
         for (var dep in devDependencies)
           if (dep.name?.text != null) dep.name!.text!,
     ];
diff --git a/pkg/linter/lib/src/rules/public_member_api_docs.dart b/pkg/linter/lib/src/rules/public_member_api_docs.dart
index d79a3e1..b0efd72 100644
--- a/pkg/linter/lib/src/rules/public_member_api_docs.dart
+++ b/pkg/linter/lib/src/rules/public_member_api_docs.dart
@@ -87,9 +87,7 @@
     if (package != null && !package.canHavePublicApi) {
       return;
     }
-    if (!isInLibDir(context.currentUnit.unit, context.package)) {
-      return;
-    }
+    if (!context.isInLibDir) return;
 
     var visitor = _Visitor(this, context);
     registry.addClassDeclaration(this, visitor);
diff --git a/pkg/linter/lib/src/rules/unnecessary_breaks.dart b/pkg/linter/lib/src/rules/unnecessary_breaks.dart
index 941b5f2..4eeee8f 100644
--- a/pkg/linter/lib/src/rules/unnecessary_breaks.dart
+++ b/pkg/linter/lib/src/rules/unnecessary_breaks.dart
@@ -78,7 +78,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!context.isEnabled(Feature.patterns)) return;
+    if (!context.libraryElement!.featureSet.isEnabled(Feature.patterns)) return;
     var visitor = _Visitor(this);
     registry.addBreakStatement(this, visitor);
   }
diff --git a/pkg/linter/lib/src/rules/unnecessary_lambdas.dart b/pkg/linter/lib/src/rules/unnecessary_lambdas.dart
index acae0cf..d901638 100644
--- a/pkg/linter/lib/src/rules/unnecessary_lambdas.dart
+++ b/pkg/linter/lib/src/rules/unnecessary_lambdas.dart
@@ -116,8 +116,8 @@
   final TypeSystem typeSystem;
 
   _Visitor(this.rule, LinterContext context)
-      : constructorTearOffsEnabled =
-            context.isEnabled(Feature.constructor_tearoffs),
+      : constructorTearOffsEnabled = context.libraryElement!.featureSet
+            .isEnabled(Feature.constructor_tearoffs),
         typeSystem = context.typeSystem;
 
   @override
diff --git a/pkg/linter/lib/src/rules/unnecessary_library_name.dart b/pkg/linter/lib/src/rules/unnecessary_library_name.dart
index fa4a1fe..b021de54 100644
--- a/pkg/linter/lib/src/rules/unnecessary_library_name.dart
+++ b/pkg/linter/lib/src/rules/unnecessary_library_name.dart
@@ -66,7 +66,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!context.currentUnit.unit.featureSet
+    if (!context.libraryElement!.featureSet
         .isEnabled(Feature.unnamedLibraries)) {
       return;
     }
diff --git a/pkg/linter/lib/src/rules/use_build_context_synchronously.dart b/pkg/linter/lib/src/rules/use_build_context_synchronously.dart
index 6d5f08b..1cf7e18 100644
--- a/pkg/linter/lib/src/rules/use_build_context_synchronously.dart
+++ b/pkg/linter/lib/src/rules/use_build_context_synchronously.dart
@@ -985,7 +985,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    var unit = context.currentUnit.unit;
+    var unit = context.definingUnit.unit;
     if (!unit.inTestDir) {
       var visitor = _Visitor(this);
       registry.addMethodInvocation(this, visitor);
diff --git a/pkg/linter/lib/src/rules/use_enums.dart b/pkg/linter/lib/src/rules/use_enums.dart
index bb74b27..f23d5d7 100644
--- a/pkg/linter/lib/src/rules/use_enums.dart
+++ b/pkg/linter/lib/src/rules/use_enums.dart
@@ -78,7 +78,9 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!context.isEnabled(Feature.enhanced_enums)) return;
+    if (!context.libraryElement!.featureSet.isEnabled(Feature.enhanced_enums)) {
+      return;
+    }
 
     var visitor = _Visitor(this, context);
     registry.addClassDeclaration(this, visitor);
diff --git a/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart b/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart
index 2359cff..09d2aaf 100644
--- a/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart
+++ b/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart
@@ -76,21 +76,40 @@
       NodeLintRegistry registry, LinterContext context) {
     var visitor = _Visitor(this, context);
     registry.addCompilationUnit(this, visitor);
+    registry.afterLibrary(this, () => visitor.afterLibrary());
   }
 }
 
 class _Visitor extends RecursiveAstVisitor<void> {
-  static final lateables =
-      <CompilationUnitElement, List<VariableDeclaration>>{};
+  final lateables = <CompilationUnitElement, List<VariableDeclaration>>{};
 
-  static final nullableAccess = <CompilationUnitElement, Set<Element>>{};
+  final nullableAccess = <Element>{};
+
   final LintRule rule;
-
   final LinterContext context;
 
-  CompilationUnitElement? currentUnit;
+  /// The "current" [CompilationUnitElement], which is set by
+  /// [visitCompilationUnit].
+  late CompilationUnitElement currentUnit;
+
   _Visitor(this.rule, this.context);
 
+  void afterLibrary() {
+    for (var contextUnit in context.allUnits) {
+      var unit = contextUnit.unit.declaredElement;
+      var variables = lateables[unit];
+      if (variables == null) continue;
+      for (var variable in variables) {
+        if (!nullableAccess.contains(variable.declaredElement)) {
+          var contextUnit = context.allUnits
+              .firstWhereOrNull((u) => u.unit.declaredElement == unit);
+          if (contextUnit == null) continue;
+          contextUnit.errorReporter.atNode(variable, rule.lintCode);
+        }
+      }
+    }
+  }
+
   @override
   void visitAssignmentExpression(AssignmentExpression node) {
     var element = node.writeElement?.canonicalElement;
@@ -104,7 +123,7 @@
           context.typeSystem.isNonNullable(rhsType)) {
         // This is OK; non-null access.
       } else {
-        nullableAccess[currentUnit]?.add(element);
+        nullableAccess.add(element);
       }
     }
     super.visitAssignmentExpression(node);
@@ -125,30 +144,10 @@
   @override
   void visitCompilationUnit(CompilationUnit node) {
     var declaredElement = node.declaredElement;
-    if (declaredElement == null) {
-      return;
-    }
-    lateables.putIfAbsent(declaredElement, () => []);
-    nullableAccess.putIfAbsent(declaredElement, () => {});
+    if (declaredElement == null) return;
     currentUnit = declaredElement;
 
     super.visitCompilationUnit(node);
-
-    var unitsInContext =
-        context.allUnits.map((e) => e.unit.declaredElement).toSet();
-    var libraryUnitsInContext =
-        declaredElement.library.units.where(unitsInContext.contains).toSet();
-    var areAllLibraryUnitsVisited =
-        libraryUnitsInContext.every(lateables.containsKey);
-    if (areAllLibraryUnitsVisited) {
-      _checkAccess(libraryUnitsInContext);
-
-      // Clean up.
-      for (var unit in libraryUnitsInContext) {
-        lateables.remove(unit);
-        nullableAccess.remove(unit);
-      }
-    }
   }
 
   @override
@@ -209,34 +208,15 @@
     super.visitTopLevelVariableDeclaration(node);
   }
 
-  void _checkAccess(Iterable<CompilationUnitElement> units) {
-    var allNullableAccess =
-        units.expand((unit) => nullableAccess[unit] ?? const {}).toSet();
-    for (var unit in units) {
-      for (var variable in lateables[unit] ?? const <VariableDeclaration>[]) {
-        if (!allNullableAccess.contains(variable.declaredElement)) {
-          var contextUnit = context.allUnits
-              .firstWhereOrNull((u) => u.unit.declaredElement == unit);
-          if (contextUnit == null) continue;
-          contextUnit.errorReporter.atNode(variable, rule.lintCode);
-        }
-      }
-    }
-  }
-
   void _visit(VariableDeclaration variable) {
-    if (variable.isLate) {
-      return;
-    }
-    if (variable.isSynthetic) {
-      return;
-    }
+    if (variable.isLate) return;
+    if (variable.isSynthetic) return;
     var declaredElement = variable.declaredElement;
     if (declaredElement == null ||
         context.typeSystem.isNonNullable(declaredElement.type)) {
       return;
     }
-    lateables[currentUnit]?.add(variable);
+    lateables.putIfAbsent(currentUnit, () => []).add(variable);
   }
 
   /// Checks whether [expression], which must be an [Identifier] or
@@ -244,20 +224,20 @@
   void _visitIdentifierOrPropertyAccess(
       Expression expression, Element? canonicalElement) {
     assert(expression is Identifier || expression is PropertyAccess);
-    if (canonicalElement != null) {
-      var parent = expression.parent;
-      if (parent is Expression) {
-        parent = parent.unParenthesized;
-      }
-      if (expression is SimpleIdentifier && expression.inDeclarationContext()) {
-        // This is OK.
-      } else if (parent is PostfixExpression &&
-          parent.operand == expression &&
-          parent.operator.type == TokenType.BANG) {
-        // This is OK; non-null access.
-      } else {
-        nullableAccess[currentUnit]?.add(canonicalElement);
-      }
+    if (canonicalElement == null) return;
+
+    var parent = expression.parent;
+    if (parent is Expression) {
+      parent = parent.unParenthesized;
+    }
+    if (expression is SimpleIdentifier && expression.inDeclarationContext()) {
+      // This is OK.
+    } else if (parent is PostfixExpression &&
+        parent.operand == expression &&
+        parent.operator.type == TokenType.BANG) {
+      // This is OK; non-null access.
+    } else {
+      nullableAccess.add(canonicalElement);
     }
   }
 }
diff --git a/pkg/linter/lib/src/rules/use_super_parameters.dart b/pkg/linter/lib/src/rules/use_super_parameters.dart
index 1b1e0c1..d2ccc0a 100644
--- a/pkg/linter/lib/src/rules/use_super_parameters.dart
+++ b/pkg/linter/lib/src/rules/use_super_parameters.dart
@@ -68,7 +68,10 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (!context.isEnabled(Feature.super_parameters)) return;
+    if (!context.libraryElement!.featureSet
+        .isEnabled(Feature.super_parameters)) {
+      return;
+    }
 
     var visitor = _Visitor(this, context);
     registry.addConstructorDeclaration(this, visitor);
diff --git a/pkg/linter/tool/checks/rules/no_solo_tests.dart b/pkg/linter/tool/checks/rules/no_solo_tests.dart
index a36b154..5328bdf 100644
--- a/pkg/linter/tool/checks/rules/no_solo_tests.dart
+++ b/pkg/linter/tool/checks/rules/no_solo_tests.dart
@@ -39,7 +39,7 @@
   @override
   void registerNodeProcessors(
       NodeLintRegistry registry, LinterContext context) {
-    if (context.currentUnit.unit.inTestDir) {
+    if (context.definingUnit.unit.inTestDir) {
       var visitor = _Visitor(this);
       registry.addMethodDeclaration(this, visitor);
     }