Merge branch 'master' into migrate-to-null-safety

# Conflicts:
#	pubspec.yaml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e44179..19d2c55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 2.0.0
+
+* Migrate to null safety.
+
 # 1.3.14
 
 * Add support for generic annotations.
diff --git a/bin/format.dart b/bin/format.dart
index c3cfee8..e757c00 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-
 import 'dart:io';
 
 import 'package:args/args.dart';
@@ -40,7 +39,7 @@
     usageError(parser, 'Can only use --verbose with --help.');
   }
 
-  List<int> selection;
+  List<int>? selection;
   try {
     selection = parseSelection(argResults, 'preserve');
   } on FormatException catch (exception) {
@@ -145,12 +144,12 @@
 }
 
 /// Prints [error] and usage help then exits with exit code 64.
-void usageError(ArgParser parser, String error) {
+Never usageError(ArgParser parser, String error) {
   printUsage(parser, error);
   exit(64);
 }
 
-void printUsage(ArgParser parser, [String error]) {
+void printUsage(ArgParser parser, [String? error]) {
   var output = stdout;
 
   var message = 'Idiomatically format Dart source code.';
diff --git a/example/format.dart b/example/format.dart
index ff1a7cd..77c79dc 100644
--- a/example/format.dart
+++ b/example/format.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.example.format;
-
 import 'dart:io';
 import 'dart:mirrors';
 
@@ -31,7 +28,8 @@
   runFormatter(source, pageWidth, isCompilationUnit: true);
 }
 
-void runFormatter(String source, int pageWidth, {bool isCompilationUnit}) {
+void runFormatter(String source, int pageWidth,
+    {required bool isCompilationUnit}) {
   try {
     var formatter = DartFormatter(pageWidth: pageWidth);
 
@@ -88,7 +86,7 @@
     var leadingIndent = 0;
     var indentMatch = indentPattern.firstMatch(description);
     if (indentMatch != null) {
-      leadingIndent = int.parse(indentMatch[1]);
+      leadingIndent = int.parse(indentMatch[1]!);
       description = description.substring(indentMatch.end);
     }
 
diff --git a/lib/dart_style.dart b/lib/dart_style.dart
index bc59401..4da21a8 100644
--- a/lib/dart_style.dart
+++ b/lib/dart_style.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style;
-
 export 'src/dart_formatter.dart';
 export 'src/exceptions.dart';
 export 'src/style_fix.dart';
diff --git a/lib/src/argument_list_visitor.dart b/lib/src/argument_list_visitor.dart
index 45f8cd9..a22864f 100644
--- a/lib/src/argument_list_visitor.dart
+++ b/lib/src/argument_list_visitor.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.argument_list_visitor;
-
 import 'dart:math' as math;
 
 import 'package:analyzer/dart/ast/ast.dart';
@@ -36,12 +33,12 @@
   /// The contiguous list of block function arguments, if any.
   ///
   /// Otherwise, this is `null`.
-  final List<Expression> _functions;
+  final List<Expression>? _functions;
 
   /// If there are block function arguments, this is the arguments after them.
   ///
   /// Otherwise, this is `null`.
-  final ArgumentSublist _argumentsAfterFunctions;
+  final ArgumentSublist? _argumentsAfterFunctions;
 
   /// Returns `true` if there is only a single positional argument.
   bool get _isSingle =>
@@ -129,16 +126,17 @@
       }
 
       for (var i = 0; i < functionsStart; i++) {
-        if (arguments[i] is! NamedExpression) continue;
+        var argument = arguments[i];
+        if (argument is! NamedExpression) continue;
 
-        if (isArrow(arguments[i])) {
+        if (isArrow(argument)) {
           functionsStart = null;
           break;
         }
       }
 
       for (var i = functionsEnd; i < arguments.length; i++) {
-        if (isArrow(arguments[i])) {
+        if (isArrow(arguments[i] as NamedExpression)) {
           functionsStart = null;
           break;
         }
@@ -174,7 +172,10 @@
       this._allArguments,
       this._arguments,
       this._functions,
-      this._argumentsAfterFunctions);
+      this._argumentsAfterFunctions) {
+    assert(_functions == null || _argumentsAfterFunctions != null,
+        'If _functions is passed, _argumentsAfterFunctions must be too.');
+  }
 
   /// Builds chunks for the argument list.
   void visit() {
@@ -189,20 +190,21 @@
 
     _visitor.builder.endSpan();
 
-    if (_functions != null) {
+    var functions = _functions;
+    if (functions != null) {
       // TODO(rnystrom): It might look better to treat the parameter list of the
       // first function as if it were an argument in the preceding argument list
       // instead of just having this little solo split here. That would try to
       // keep the parameter list with other arguments when possible, and, I
       // think, generally look nicer.
-      if (_functions.first == _allArguments.first) {
+      if (functions.first == _allArguments.first) {
         _visitor.soloZeroSplit();
       } else {
         _visitor.soloSplit();
       }
 
-      for (var argument in _functions) {
-        if (argument != _functions.first) _visitor.space();
+      for (var argument in functions) {
+        if (argument != functions.first) _visitor.space();
 
         _visitor.visit(argument);
 
@@ -213,7 +215,7 @@
       }
 
       _visitor.builder.startSpan();
-      _argumentsAfterFunctions.visit(_visitor);
+      _argumentsAfterFunctions!.visit(_visitor);
       _visitor.builder.endSpan();
     }
 
@@ -225,9 +227,7 @@
   /// Returns `true` if [expression] is a [FunctionExpression] with a non-empty
   /// block body.
   static bool _isBlockFunction(Expression expression) {
-    if (expression is NamedExpression) {
-      expression = (expression as NamedExpression).expression;
-    }
+    if (expression is NamedExpression) expression = expression.expression;
 
     // Allow functions wrapped in dotted method calls like "a.b.c(() { ... })".
     if (expression is MethodInvocation) {
@@ -245,39 +245,37 @@
 
     // Allow immediately-invoked functions like "() { ... }()".
     if (expression is FunctionExpressionInvocation) {
-      var invocation = expression as FunctionExpressionInvocation;
-      if (invocation.argumentList.arguments.isNotEmpty) return false;
+      if (expression.argumentList.arguments.isNotEmpty) return false;
 
-      expression = invocation.function;
+      expression = expression.function;
     }
 
     // Unwrap parenthesized expressions.
     while (expression is ParenthesizedExpression) {
-      expression = (expression as ParenthesizedExpression).expression;
+      expression = expression.expression;
     }
 
     // Must be a function.
     if (expression is! FunctionExpression) return false;
 
     // With a curly body.
-    var function = expression as FunctionExpression;
-    if (function.body is! BlockFunctionBody) return false;
+    if (expression.body is! BlockFunctionBody) return false;
 
     // That isn't empty.
-    var body = function.body as BlockFunctionBody;
+    var body = expression.body as BlockFunctionBody;
     return body.block.statements.isNotEmpty ||
         body.block.rightBracket.precedingComments != null;
   }
 
   /// Returns `true` if [expression] is a valid method invocation target for
   /// an invocation that wraps a function literal argument.
-  static bool _isValidWrappingTarget(Expression expression) {
+  static bool _isValidWrappingTarget(Expression? expression) {
     // Allow bare function calls.
     if (expression == null) return true;
 
     // Allow property accesses.
     while (expression is PropertyAccess) {
-      expression = (expression as PropertyAccess).target;
+      expression = expression.target;
     }
 
     if (expression is PrefixedIdentifier) return true;
@@ -318,12 +316,12 @@
   final int _trailingBlocks;
 
   /// The rule used to split the bodies of all block arguments.
-  Rule get blockRule => _blockRule;
-  Rule _blockRule;
+  Rule? get blockRule => _blockRule;
+  Rule? _blockRule;
 
   /// The most recent chunk that split before an argument.
-  Chunk get previousSplit => _previousSplit;
-  Chunk _previousSplit;
+  Chunk? get previousSplit => _previousSplit;
+  Chunk? _previousSplit;
 
   factory ArgumentSublist(
       List<Expression> allArguments, List<Expression> arguments) {
@@ -381,7 +379,7 @@
   }
 
   /// Writes the positional arguments, if any.
-  PositionalRule _visitPositional(SourceVisitor visitor) {
+  PositionalRule? _visitPositional(SourceVisitor visitor) {
     if (_positional.isEmpty) return null;
 
     // Allow splitting after "(".
@@ -395,7 +393,7 @@
   }
 
   /// Writes the named arguments, if any.
-  void _visitNamed(SourceVisitor visitor, PositionalRule positionalRule) {
+  void _visitNamed(SourceVisitor visitor, PositionalRule? positionalRule) {
     if (_named.isEmpty) return;
 
     // Only count the blocks in the named rule.
@@ -443,11 +441,12 @@
   void _visitArgument(
       SourceVisitor visitor, ArgumentRule rule, Expression argument) {
     // If we're about to write a block argument, handle it specially.
-    if (_blocks.containsKey(argument)) {
+    var argumentBlock = _blocks[argument];
+    if (argumentBlock != null) {
       rule.disableSplitOnInnerRules();
 
       // Tell it to use the rule we've already created.
-      visitor.beforeBlock(_blocks[argument], blockRule, previousSplit);
+      visitor.beforeBlock(argumentBlock, blockRule!, previousSplit);
     } else if (_allArguments.length > 1) {
       // Edge case: Only bump the nesting if there are multiple arguments. This
       // lets us avoid spurious indentation in cases like:
@@ -471,7 +470,7 @@
       visitor.visit(argument);
     }
 
-    if (_blocks.containsKey(argument)) {
+    if (argumentBlock != null) {
       rule.enableSplitOnInnerRules();
     } else if (_allArguments.length > 1) {
       visitor.builder.endBlockArgumentNesting();
@@ -490,9 +489,9 @@
   ///
   /// Block-formatted arguments can get special indentation to make them look
   /// more statement-like.
-  static Token _blockToken(Expression expression) {
+  static Token? _blockToken(Expression expression) {
     if (expression is NamedExpression) {
-      expression = (expression as NamedExpression).expression;
+      expression = expression.expression;
     }
 
     // TODO(rnystrom): Should we step into parenthesized expressions?
diff --git a/lib/src/call_chain_visitor.dart b/lib/src/call_chain_visitor.dart
index 80b1b51..5af43f7 100644
--- a/lib/src/call_chain_visitor.dart
+++ b/lib/src/call_chain_visitor.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.call_chain_visitor;
-
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 
@@ -81,7 +78,7 @@
   ///         })
   ///         .d()
   ///         .e();
-  final List<_MethodSelector> _blockCalls;
+  final List<_MethodSelector>? _blockCalls;
 
   /// If there is one or more block calls and a single chained expression after
   /// that, this will be that expression.
@@ -96,7 +93,7 @@
   /// need to split before its `.` and this accommodates the common pattern of
   /// a trailing `toList()` or `toSet()` after a series of higher-order methods
   /// on an iterable.
-  final _Selector _hangingCall;
+  final _Selector? _hangingCall;
 
   /// Whether or not a [Rule] is currently active for the call chain.
   bool _ruleEnabled = false;
@@ -106,7 +103,7 @@
 
   /// After the properties are visited (if there are any), this will be the
   /// rule used to split between them.
-  PositionalRule _propertyRule;
+  PositionalRule? _propertyRule;
 
   /// Creates a new call chain visitor for [visitor] for the method chain
   /// contained in [node].
@@ -135,15 +132,15 @@
     calls.removeRange(0, properties.length);
 
     // Separate out the block calls, if there are any.
-    List<_MethodSelector> blockCalls;
-    _Selector hangingCall;
+    List<_MethodSelector>? blockCalls;
+    _Selector? hangingCall;
 
     var inBlockCalls = false;
     for (var call in calls) {
       if (call.isBlockCall(visitor)) {
         inBlockCalls = true;
         blockCalls ??= [];
-        blockCalls.add(call);
+        blockCalls.add(call as _MethodSelector);
       } else if (inBlockCalls) {
         // We found a non-block call after a block call.
         if (call == calls.last) {
@@ -181,7 +178,7 @@
   /// created for the call chain and the caller must end it. Used by cascades
   /// to force a cascade after a method chain to be more deeply nested than
   /// the methods.
-  void visit({bool unnest}) {
+  void visit({bool? unnest}) {
     unnest ??= true;
 
     _visitor.builder.nestExpression();
@@ -216,7 +213,7 @@
       }
 
       for (var property in _properties) {
-        _propertyRule.beforeArgument(_visitor.zeroSplit());
+        _propertyRule!.beforeArgument(_visitor.zeroSplit());
         property.write(this);
       }
 
@@ -245,12 +242,13 @@
 
     // If there are block calls, end the chain and write those without any
     // extra indentation.
-    if (_blockCalls != null) {
+    var blockCalls = _blockCalls;
+    if (blockCalls != null) {
       _enableRule();
       _visitor.zeroSplit();
       _disableRule();
 
-      for (var blockCall in _blockCalls) {
+      for (var blockCall in blockCalls) {
         blockCall.write(this);
       }
 
@@ -289,7 +287,7 @@
 
     // Unwrap parentheses.
     while (expression is ParenthesizedExpression) {
-      expression = (expression as ParenthesizedExpression).expression;
+      expression = expression.expression;
     }
 
     // Don't split right after a collection literal.
@@ -305,7 +303,7 @@
 
     // If the expression ends in an argument list, base the splitting on the
     // last argument.
-    ArgumentList argumentList;
+    ArgumentList? argumentList;
     if (expression is MethodInvocation) {
       argumentList = expression.argumentList;
     } else if (expression is InstanceCreationExpression) {
@@ -324,7 +322,7 @@
     if (_visitor.hasCommaAfter(argument)) return false;
 
     if (argument is NamedExpression) {
-      argument = (argument as NamedExpression).expression;
+      argument = argument.expression;
     }
 
     // TODO(rnystrom): This logic is similar (but not identical) to
@@ -386,7 +384,7 @@
 
     // If the properties split, force the calls to split too.
     var rule = Rule();
-    if (_propertyRule != null) _propertyRule.setNamedArgsRule(rule);
+    _propertyRule?.setNamedArgsRule(rule);
 
     if (lazy) {
       _visitor.builder.startLazyRule(rule);
@@ -548,11 +546,11 @@
 
   // Selectors.
   if (node is MethodInvocation && node.target != null) {
-    return _unwrapSelector(node.target, _MethodSelector(node), calls);
+    return _unwrapSelector(node.target!, _MethodSelector(node), calls);
   }
 
   if (node is PropertyAccess && node.target != null) {
-    return _unwrapSelector(node.target, _PropertySelector(node), calls);
+    return _unwrapSelector(node.target!, _PropertySelector(node), calls);
   }
 
   if (node is PrefixedIdentifier) {
@@ -561,7 +559,7 @@
 
   // Postfix expressions.
   if (node is IndexExpression) {
-    return _unwrapPostfix(node, node.target, calls);
+    return _unwrapPostfix(node, node.target!, calls);
   }
 
   if (node is FunctionExpressionInvocation) {
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 30a6d92..02dbfb6 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.chunk;
-
 import 'fast_hash.dart';
 import 'nesting_level.dart';
 import 'rule/rule.dart';
@@ -16,13 +13,13 @@
 
   /// The offset from the beginning of [text] where the selection starts, or
   /// `null` if the selection does not start within this chunk.
-  int get selectionStart => _selectionStart;
-  int _selectionStart;
+  int? get selectionStart => _selectionStart;
+  int? _selectionStart;
 
   /// The offset from the beginning of [text] where the selection ends, or
   /// `null` if the selection does not start within this chunk.
-  int get selectionEnd => _selectionEnd;
-  int _selectionEnd;
+  int? get selectionEnd => _selectionEnd;
+  int? _selectionEnd;
 
   /// Sets [selectionStart] to be [start] characters into [text].
   void startSelection(int start) {
@@ -77,8 +74,8 @@
   ///
   /// For top level chunks that are not inside any block, this also includes
   /// leading indentation.
-  int get indent => _indent;
-  int _indent;
+  int? get indent => _indent;
+  int? _indent;
 
   /// The expression nesting level following this chunk.
   ///
@@ -91,12 +88,14 @@
   ///         argument, anotherFunction(argument,
   ///             argument));
   NestingLevel get nesting => _nesting;
-  NestingLevel _nesting;
+  late NestingLevel _nesting;
 
   /// If this chunk marks the beginning of a block, this contains the child
   /// chunks and other data about that nested block.
-  ChunkBlock get block => _block;
-  ChunkBlock _block;
+  ///
+  /// This should only be accessed when [isBlock] is `true`.
+  ChunkBlock get block => _block!;
+  ChunkBlock? _block;
 
   /// Whether this chunk has a [block].
   bool get isBlock => _block != null;
@@ -112,8 +111,8 @@
   /// The [Rule] that controls when a split should occur after this chunk.
   ///
   /// Multiple splits may share a [Rule].
-  Rule get rule => _rule;
-  Rule _rule;
+  Rule? get rule => _rule;
+  Rule? _rule;
 
   /// Whether or not an extra blank line should be output after this chunk if
   /// it's split.
@@ -125,7 +124,7 @@
   /// However, this getter does not expose that. It will return `false` if the
   /// chunk is still indeterminate.
   bool get isDouble => _isDouble ?? false;
-  bool _isDouble;
+  bool? _isDouble;
 
   /// If `true`, then the line after this chunk should always be at column
   /// zero regardless of any indentation or expression nesting.
@@ -139,7 +138,7 @@
   bool get flushLeftAfter {
     if (!isBlock) return _flushLeft;
 
-    return _block.chunks.last.flushLeftAfter;
+    return block.chunks.last.flushLeftAfter;
   }
 
   /// Whether this chunk should append an extra space if it does not split.
@@ -150,13 +149,10 @@
 
   /// Whether this chunk marks the end of a range of chunks that can be line
   /// split independently of the following chunks.
-  bool get canDivide {
-    // Have to call markDivide() before accessing this.
-    assert(_canDivide != null);
-    return _canDivide;
-  }
-
-  bool _canDivide;
+  ///
+  /// You must call markDivide() before accessing this.
+  bool get canDivide => _canDivide;
+  late final bool _canDivide;
 
   /// The number of characters in this chunk when unsplit.
   int get length => _text.length + (spaceWhenUnsplit ? 1 : 0);
@@ -166,10 +162,10 @@
   /// Does not include this chunk's own length, just the length of its child
   /// block chunks (recursively).
   int get unsplitBlockLength {
-    if (_block == null) return 0;
+    if (!isBlock) return 0;
 
     var length = 0;
-    for (var chunk in _block.chunks) {
+    for (var chunk in block.chunks) {
       length += chunk.length + chunk.unsplitBlockLength;
     }
 
@@ -201,7 +197,7 @@
   /// preserved whitespace often overlap. When that happens, this has logic to
   /// combine that information into a single split.
   void applySplit(Rule rule, int indent, NestingLevel nesting,
-      {bool flushLeft, bool isDouble, bool space}) {
+      {bool? flushLeft, bool? isDouble, bool? space}) {
     flushLeft ??= false;
     space ??= false;
     if (rule.isHardened) {
@@ -223,7 +219,7 @@
   }
 
   /// Turns this chunk into one that can contain a block of child chunks.
-  void makeBlock(Chunk blockArgument) {
+  void makeBlock(Chunk? blockArgument) {
     assert(_block == null);
     _block = ChunkBlock(blockArgument);
   }
@@ -231,18 +227,16 @@
   /// Returns `true` if the block body owned by this chunk should be expression
   /// indented given a set of rule values provided by [getValue].
   bool indentBlock(int Function(Rule) getValue) {
-    if (_block == null) return false;
-    if (_block.argument == null) return false;
+    if (!isBlock) return false;
 
-    return _block.argument.rule
-        .isSplit(getValue(_block.argument.rule), _block.argument);
+    var argument = block.argument;
+    if (argument == null) return false;
+
+    return argument.rule!.isSplit(getValue(argument.rule!), argument);
   }
 
   // Mark whether this chunk can divide the range of chunks.
-  void markDivide(canDivide) {
-    // Should only do this once.
-    assert(_canDivide == null);
-
+  void markDivide(bool canDivide) {
     _canDivide = canDivide;
   }
 
@@ -257,14 +251,15 @@
     if (_isDouble == true) parts.add('double');
     if (_flushLeft == true) parts.add('flush');
 
-    if (_rule == null) {
+    var rule = _rule;
+    if (rule == null) {
       parts.add('(no split)');
     } else {
       parts.add(rule.toString());
       if (rule.isHardened) parts.add('(hard)');
 
-      if (_rule.constrainedRules.isNotEmpty) {
-        parts.add("-> ${_rule.constrainedRules.join(' ')}");
+      if (rule.constrainedRules.isNotEmpty) {
+        parts.add("-> ${rule.constrainedRules.join(' ')}");
       }
     }
 
@@ -280,7 +275,7 @@
   ///
   /// That chunk is owned by the argument list and if it splits, this collection
   /// may need extra expression-level indentation.
-  final Chunk argument;
+  final Chunk? argument;
 
   /// The child chunks in this block.
   final List<Chunk> chunks = [];
@@ -403,5 +398,5 @@
   bool get isInline => linesBefore == 0 && !isLineComment;
 
   SourceComment(this.text, this.linesBefore,
-      {this.isLineComment, this.flushLeft});
+      {required this.isLineComment, required this.flushLeft});
 }
diff --git a/lib/src/chunk_builder.dart b/lib/src/chunk_builder.dart
index ec1c589..a17c89a 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -39,7 +39,7 @@
 
   /// The builder for the code surrounding the block that this writer is for, or
   /// `null` if this is writing the top-level code.
-  final ChunkBuilder _parent;
+  final ChunkBuilder? _parent;
 
   final SourceCode _source;
 
@@ -181,7 +181,7 @@
   /// If [nest] is `false`, ignores any current expression nesting. Otherwise,
   /// uses the current nesting level. If unsplit, it expands to a space if
   /// [space] is `true`.
-  Chunk split({bool flushLeft, bool isDouble, bool nest, bool space}) {
+  Chunk? split({bool? flushLeft, bool? isDouble, bool? nest, bool? space}) {
     space ??= false;
 
     // If we are not allowed to split at all, don't. Returning null for the
@@ -243,9 +243,9 @@
     //     new // b
     //     Foo();
     //
-    // When that happens, we need to make sure the preserve the split at the
-    // end of the first sequence of comments if there is one.
-    if (_pendingWhitespace == null) {
+    // When that happens, we need to make sure to preserve the split at the end
+    // of the first sequence of comments if there is one.
+    if (_pendingWhitespace == Whitespace.afterHardSplit) {
       comments.first.linesBefore = 1;
       _pendingWhitespace = Whitespace.none;
     }
@@ -305,11 +305,11 @@
       _writeCommentText(comment);
 
       if (comment.selectionStart != null) {
-        startSelectionFromEnd(comment.text.length - comment.selectionStart);
+        startSelectionFromEnd(comment.text.length - comment.selectionStart!);
       }
 
       if (comment.selectionEnd != null) {
-        endSelectionFromEnd(comment.text.length - comment.selectionEnd);
+        endSelectionFromEnd(comment.text.length - comment.selectionEnd!);
       }
 
       // Make sure there is at least one newline after a line comment and allow
@@ -359,7 +359,7 @@
       return;
     }
 
-    var lines = match.group(1).split('\n').toList();
+    var lines = match[1]!.split('\n').toList();
     var leastIndentation = comment.text.length;
 
     for (var i = 0; i < lines.length; i++) {
@@ -370,13 +370,13 @@
       if (i > 0 && i < lines.length - 1) {
         var match = _javaDocLine.firstMatch(line);
         if (match != null) {
-          line = match.group(1);
+          line = match[1]!;
         }
       }
 
       // Find the line with the least indentation.
       if (line.isNotEmpty) {
-        var indentation = _leadingIndentation.firstMatch(line).group(1).length;
+        var indentation = _leadingIndentation.firstMatch(line)![1]!.length;
         leastIndentation = math.min(leastIndentation, indentation);
       }
 
@@ -440,7 +440,7 @@
   /// Creates a new indentation level [spaces] deeper than the current one.
   ///
   /// If omitted, [spaces] defaults to [Indent.block].
-  void indent([int spaces]) {
+  void indent([int? spaces]) {
     _nesting.indent(spaces);
   }
 
@@ -468,14 +468,14 @@
     var span = Span(openSpan.cost);
     for (var i = openSpan.start; i < end; i++) {
       var chunk = _chunks[i];
-      if (!chunk.rule.isHardened) chunk.spans.add(span);
+      if (!chunk.rule!.isHardened) chunk.spans.add(span);
     }
   }
 
   /// Starts a new [Rule].
   ///
   /// If omitted, defaults to a new [Rule].
-  void startRule([Rule rule]) {
+  void startRule([Rule? rule]) {
     rule ??= Rule();
 
     // If there are any pending lazy rules, start them now so that the proper
@@ -503,7 +503,7 @@
   /// entire expression.
   ///
   /// If [rule] is omitted, defaults to a new [Rule].
-  void startLazyRule([Rule rule]) {
+  void startLazyRule([Rule? rule]) {
     rule ??= Rule();
 
     _lazyRules.add(rule);
@@ -536,7 +536,7 @@
   /// If [indent] is omitted, defaults to [Indent.expression]. If [now] is
   /// `true`, commits the nesting change immediately instead of waiting until
   /// after the next chunk of text is written.
-  void nestExpression({int indent, bool now}) {
+  void nestExpression({int? indent, bool? now}) {
     now ??= false;
 
     _nesting.nest(indent);
@@ -550,7 +550,7 @@
   ///
   /// If [now] is `false`, does not commit the nesting change until after the
   /// next chunk of text is written.
-  void unnest({bool now}) {
+  void unnest({bool? now}) {
     now ??= true;
 
     _nesting.unnest();
@@ -591,7 +591,7 @@
   /// Starts a new block as a child of the current chunk.
   ///
   /// Nested blocks are handled using their own independent [LineWriter].
-  ChunkBuilder startBlock(Chunk argumentChunk) {
+  ChunkBuilder startBlock(Chunk? argumentChunk) {
     var chunk = _chunks.last;
     chunk.makeBlock(argumentChunk);
 
@@ -612,7 +612,7 @@
   /// `true`, the block is considered to always split.
   ///
   /// Returns the previous writer for the surrounding block.
-  ChunkBuilder endBlock(Rule ignoredSplit, {bool forceSplit}) {
+  ChunkBuilder endBlock(Rule? ignoredSplit, {required bool forceSplit}) {
     _divideChunks();
 
     // If we don't already know if the block is going to split, see if it
@@ -627,7 +627,7 @@
         }
 
         if (chunk.rule != null &&
-            chunk.rule.isHardened &&
+            chunk.rule!.isHardened &&
             chunk.rule != ignoredSplit) {
           forceSplit = true;
           break;
@@ -635,14 +635,13 @@
       }
     }
 
-    _parent._endChildBlock(
+    _parent!._endChildBlock(
         firstFlushLeft: _firstFlushLeft, forceSplit: forceSplit);
-
-    return _parent;
+    return _parent!;
   }
 
   /// Finishes off the last chunk in a child block of this parent.
-  void _endChildBlock({bool firstFlushLeft, bool forceSplit}) {
+  void _endChildBlock({bool? firstFlushLeft, required bool forceSplit}) {
     // If there is a hard newline within the block, force the surrounding rule
     // for it so that we apply that constraint.
     if (forceSplit) forceRules();
@@ -652,7 +651,7 @@
     chunk.applySplit(rule, _nesting.indentation, _blockArgumentNesting.last,
         flushLeft: firstFlushLeft);
 
-    if (chunk.rule.isHardened) _handleHardSplit();
+    if (chunk.rule!.isHardened) _handleHardSplit();
   }
 
   /// Finishes writing and returns a [SourceCode] containing the final output
@@ -838,9 +837,9 @@
   /// If [flushLeft] is `true`, then the split will always cause the next line
   /// to be at column zero. Otherwise, it uses the normal indentation and
   /// nesting behavior.
-  void _writeHardSplit({bool isDouble, bool flushLeft, bool nest = false}) {
+  void _writeHardSplit({bool? isDouble, bool? flushLeft, bool nest = false}) {
     // A hard split overrides any other whitespace.
-    _pendingWhitespace = null;
+    _pendingWhitespace = Whitespace.afterHardSplit;
     _writeSplit(Rule.hard(),
         flushLeft: flushLeft, isDouble: isDouble, nest: nest);
   }
@@ -848,8 +847,8 @@
   /// Ends the current chunk (if any) with the given split information.
   ///
   /// Returns the chunk.
-  Chunk _writeSplit(Rule rule,
-      {bool flushLeft, bool isDouble, bool nest, bool space}) {
+  Chunk? _writeSplit(Rule rule,
+      {bool? flushLeft, bool? isDouble, bool? nest, bool? space}) {
     nest ??= true;
     space ??= false;
 
@@ -863,7 +862,7 @@
         rule, _nesting.indentation, nest ? _nesting.nesting : NestingLevel(),
         flushLeft: flushLeft, isDouble: isDouble, space: space);
 
-    if (_chunks.last.rule.isHardened) _handleHardSplit();
+    if (_chunks.last.rule!.isHardened) _handleHardSplit();
     return _chunks.last;
   }
 
@@ -884,7 +883,7 @@
     if (i == _chunks.length - 1) return false;
 
     var chunk = _chunks[i];
-    if (!chunk.rule.isHardened) return false;
+    if (!chunk.rule!.isHardened) return false;
     if (chunk.nesting.isNested) return false;
     if (chunk.isBlock) return false;
 
@@ -950,7 +949,7 @@
     // Discard spans in hardened chunks since we know for certain they will
     // split anyway.
     for (var chunk in _chunks) {
-      if (chunk.rule != null && chunk.rule.isHardened) {
+      if (chunk.rule != null && chunk.rule!.isHardened) {
         chunk.spans.clear();
       }
     }
diff --git a/lib/src/cli/format_command.dart b/lib/src/cli/format_command.dart
index 5b14a7e..6f6627e 100644
--- a/lib/src/cli/format_command.dart
+++ b/lib/src/cli/format_command.dart
@@ -22,7 +22,7 @@
 
   @override
   String get invocation =>
-      '${runner.executableName} $name [options...] <files or directories...>';
+      '${runner!.executableName} $name [options...] <files or directories...>';
 
   FormatCommand({bool verbose = false}) {
     defineOptions(argParser, oldCli: false, verbose: verbose);
@@ -30,6 +30,8 @@
 
   @override
   Future<int> run() async {
+    var argResults = this.argResults!;
+
     if (argResults['version']) {
       print(dartStyleVersion);
       return 0;
@@ -39,14 +41,14 @@
       'all': Show.all,
       'changed': Show.changed,
       'none': Show.none
-    }[argResults['show']];
+    }[argResults['show']]!;
 
     var output = const {
       'write': Output.write,
       'show': Output.show,
       'none': Output.none,
       'json': Output.json,
-    }[argResults['output']];
+    }[argResults['output']]!;
 
     var summary = Summary.none;
     switch (argResults['summary'] as String) {
@@ -114,7 +116,7 @@
       }
     }
 
-    List<int> selection;
+    List<int>? selection;
     try {
       selection = parseSelection(argResults, 'selection');
     } on FormatException catch (exception) {
diff --git a/lib/src/cli/formatter_options.dart b/lib/src/cli/formatter_options.dart
index d2972a8..4748aa4 100644
--- a/lib/src/cli/formatter_options.dart
+++ b/lib/src/cli/formatter_options.dart
@@ -28,7 +28,7 @@
   final bool followLinks;
 
   /// The style fixes to apply while formatting.
-  final Iterable<StyleFix> fixes;
+  final List<StyleFix> fixes;
 
   /// Which affected files should be shown.
   final Show show;
@@ -45,14 +45,17 @@
       {this.indent = 0,
       this.pageWidth = 80,
       this.followLinks = false,
-      this.fixes,
+      Iterable<StyleFix>? fixes,
       this.show = Show.changed,
       this.output = Output.write,
       this.summary = Summary.none,
-      this.setExitIfChanged = false});
+      this.setExitIfChanged = false})
+      : fixes = [...?fixes];
 
   /// Called when [file] is about to be formatted.
-  void beforeFile(File file, String label) {
+  ///
+  /// If stdin is being formatted, then [file] is `null`.
+  void beforeFile(File? file, String label) {
     summary.beforeFile(file, label);
   }
 
@@ -60,8 +63,10 @@
   ///
   /// If the contents of the file are the same as the formatted output,
   /// [changed] will be false.
-  void afterFile(File file, String displayPath, SourceCode result,
-      {bool changed}) {
+  ///
+  /// If stdin is being formatted, then [file] is `null`.
+  void afterFile(File? file, String displayPath, SourceCode result,
+      {required bool changed}) {
     summary.afterFile(this, file, displayPath, result, changed: changed);
 
     // Save the results to disc.
diff --git a/lib/src/cli/options.dart b/lib/src/cli/options.dart
index 30ebcc8..94f999f 100644
--- a/lib/src/cli/options.dart
+++ b/lib/src/cli/options.dart
@@ -124,7 +124,7 @@
   }
 }
 
-List<int> parseSelection(ArgResults argResults, String optionName) {
+List<int>? parseSelection(ArgResults argResults, String optionName) {
   var option = argResults[optionName];
   if (option == null) return null;
 
diff --git a/lib/src/cli/output.dart b/lib/src/cli/output.dart
index da8d241..f7d9bd7 100644
--- a/lib/src/cli/output.dart
+++ b/lib/src/cli/output.dart
@@ -24,7 +24,9 @@
   const Output._();
 
   /// Write the file to disc.
-  bool writeFile(File file, String displayPath, SourceCode result) => false;
+  ///
+  /// If stdin is being formatted, then [file] is `null`.
+  bool writeFile(File? file, String displayPath, SourceCode result) => false;
 
   /// Print the file to the terminal in some way.
   void showFile(String path, SourceCode result) {}
@@ -34,12 +36,12 @@
   const _WriteOutput() : super._();
 
   @override
-  bool writeFile(File file, String displayPath, SourceCode result) {
+  bool writeFile(File? file, String displayPath, SourceCode result) {
     try {
-      file.writeAsStringSync(result.text);
+      file!.writeAsStringSync(result.text);
     } on FileSystemException catch (err) {
       stderr.writeln('Could not overwrite $displayPath: '
-          '${err.osError.message} (error code ${err.osError.errorCode})');
+          '${err.osError!.message} (error code ${err.osError!.errorCode})');
     }
 
     return true;
diff --git a/lib/src/cli/show.dart b/lib/src/cli/show.dart
index 810759f..e3a8863 100644
--- a/lib/src/cli/show.dart
+++ b/lib/src/cli/show.dart
@@ -36,7 +36,8 @@
   /// Describes a file that was processed.
   ///
   /// Returns whether or not this file should be displayed.
-  bool file(String path, {bool changed, bool overwritten}) => true;
+  bool file(String path, {required bool changed, required bool overwritten}) =>
+      true;
 
   /// Describes the directory whose contents are about to be processed.
   void directory(String path) {}
@@ -47,7 +48,7 @@
   /// Describes the hidden [path] that wasn't processed.
   void hiddenPath(String path) {}
 
-  void _showFileChange(String path, {bool overwritten}) {
+  void _showFileChange(String path, {required bool overwritten}) {
     if (overwritten) {
       print('Formatted $path');
     } else {
@@ -58,7 +59,7 @@
 
 mixin _ShowFileMixin on Show {
   @override
-  bool file(String path, {bool changed, bool overwritten}) {
+  bool file(String path, {required bool changed, required bool overwritten}) {
     if (changed) {
       _showFileChange(path, overwritten: overwritten);
     } else {
@@ -102,7 +103,7 @@
   const _ChangedShow() : super._();
 
   @override
-  bool file(String path, {bool changed, bool overwritten}) {
+  bool file(String path, {required bool changed, required bool overwritten}) {
     if (changed) _showFileChange(path, overwritten: overwritten);
     return changed;
   }
@@ -124,7 +125,7 @@
       p.relative(file, from: directory);
 
   @override
-  bool file(String path, {bool changed, bool overwritten}) {
+  bool file(String path, {required bool changed, required bool overwritten}) {
     if (changed) print(path);
     return true;
   }
diff --git a/lib/src/cli/summary.dart b/lib/src/cli/summary.dart
index 2d238ab..f1fc122 100644
--- a/lib/src/cli/summary.dart
+++ b/lib/src/cli/summary.dart
@@ -22,15 +22,19 @@
   const Summary._();
 
   /// Called when [file] is about to be formatted.
-  void beforeFile(File file, String displayPath) {}
+  ///
+  /// If stdin is being formatted, then [file] is `null`.
+  void beforeFile(File? file, String displayPath) {}
 
   /// Describe the processed file at [path] whose formatted result is [output].
   ///
   /// If the contents of the file are the same as the formatted output,
   /// [changed] will be false.
-  void afterFile(FormatterOptions options, File file, String displayPath,
+  ///
+  /// If stdin is being formatted, then [file] is `null`.
+  void afterFile(FormatterOptions options, File? file, String displayPath,
       SourceCode output,
-      {bool changed}) {}
+      {required bool changed}) {}
 
   void show() {}
 }
@@ -52,9 +56,9 @@
   /// If the contents of the file are the same as the formatted output,
   /// [changed] will be false.
   @override
-  void afterFile(FormatterOptions options, File file, String displayPath,
+  void afterFile(FormatterOptions options, File? file, String displayPath,
       SourceCode output,
-      {bool changed}) {
+      {required bool changed}) {
     _files++;
     if (changed) _changed++;
   }
@@ -96,7 +100,7 @@
     assert(_ongoing.isEmpty);
 
     var files = _elapsed.keys.toList();
-    files.sort((a, b) => _elapsed[b].compareTo(_elapsed[a]));
+    files.sort((a, b) => _elapsed[b]!.compareTo(_elapsed[a]!));
 
     for (var file in files) {
       print('${_elapsed[file]}: $file');
@@ -110,7 +114,7 @@
 
   /// Called when [file] is about to be formatted.
   @override
-  void beforeFile(File file, String displayPath) {
+  void beforeFile(File? file, String displayPath) {
     _ongoing[displayPath] = DateTime.now();
   }
 
@@ -119,10 +123,10 @@
   /// If the contents of the file are the same as the formatted output,
   /// [changed] will be false.
   @override
-  void afterFile(FormatterOptions options, File file, String displayPath,
+  void afterFile(FormatterOptions options, File? file, String displayPath,
       SourceCode output,
-      {bool changed}) {
-    var elapsed = DateTime.now().difference(_ongoing.remove(displayPath));
+      {required bool changed}) {
+    var elapsed = DateTime.now().difference(_ongoing.remove(displayPath)!);
     if (elapsed.inMilliseconds >= 10) {
       _elapsed[displayPath] = elapsed;
     } else {
diff --git a/lib/src/dart_formatter.dart b/lib/src/dart_formatter.dart
index e842bb2..eac9377 100644
--- a/lib/src/dart_formatter.dart
+++ b/lib/src/dart_formatter.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.dart_formatter;
-
 import 'dart:math' as math;
 
 import 'package:analyzer/dart/analysis/features.dart';
@@ -29,7 +26,7 @@
   /// If not explicitly provided, this is inferred from the source text. If the
   /// first newline is `\r\n` (Windows), it will use that. Otherwise, it uses
   /// Unix-style line endings (`\n`).
-  String lineEnding;
+  String? lineEnding;
 
   /// The number of characters allowed in a single line.
   final int pageWidth;
@@ -37,7 +34,7 @@
   /// The number of characters of indentation to prefix the output lines with.
   final int indent;
 
-  final Set<StyleFix> fixes = {};
+  final Set<StyleFix> fixes;
 
   /// Creates a new formatter for Dart code.
   ///
@@ -50,11 +47,10 @@
   ///
   /// While formatting, also applies any of the given [fixes].
   DartFormatter(
-      {this.lineEnding, int pageWidth, int indent, Iterable<StyleFix> fixes})
+      {this.lineEnding, int? pageWidth, int? indent, Iterable<StyleFix>? fixes})
       : pageWidth = pageWidth ?? 80,
-        indent = indent ?? 0 {
-    if (fixes != null) this.fixes.addAll(fixes);
-  }
+        indent = indent ?? 0,
+        fixes = {...?fixes};
 
   /// Formats the given [source] string containing an entire Dart compilation
   /// unit.
@@ -106,7 +102,7 @@
         uri: source.uri,
         isCompilationUnit: false,
         selectionStart: source.selectionStart != null
-            ? source.selectionStart + inputOffset
+            ? source.selectionStart! + inputOffset
             : null,
         selectionLength: source.selectionLength,
       );
@@ -151,7 +147,7 @@
       node = body.block.statements[0];
 
       // Make sure we consumed all of the source.
-      var token = node.endToken.next;
+      var token = node.endToken.next!;
       if (token.type != TokenType.CLOSE_CURLY_BRACKET) {
         var stringSource = StringSource(text, source.uri);
         var error = AnalysisError(
diff --git a/lib/src/debug.dart b/lib/src/debug.dart
index 9068af9..5934afd 100644
--- a/lib/src/debug.dart
+++ b/lib/src/debug.dart
@@ -3,12 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Internal debugging utilities.
-library dart_style.src.debug;
-
 import 'dart:math' as math;
 
 import 'chunk.dart';
 import 'line_splitting/rule_set.dart';
+import 'rule/rule.dart';
 
 /// Set this to `true` to turn on diagnostic output while building chunks.
 bool traceChunkBuilder = false;
@@ -127,19 +126,19 @@
       }
     }
 
-    if (chunk.rule == null) {
+    var rule = chunk.rule;
+    if (rule == null) {
       row.add('');
       row.add('(no rule)');
       row.add('');
     } else {
-      writeIf(chunk.rule.cost != 0, () => '\$${chunk.rule.cost}');
+      writeIf(rule.cost != 0, () => '\$${rule.cost}');
 
-      var ruleString = chunk.rule.toString();
-      if (chunk.rule.isHardened) ruleString += '!';
+      var ruleString = rule.toString();
+      if (rule.isHardened) ruleString += '!';
       row.add(ruleString);
 
-      var constrainedRules =
-          chunk.rule.constrainedRules.toSet().intersection(rules);
+      var constrainedRules = rule.constrainedRules.toSet().intersection(rules);
       writeIf(constrainedRules.isNotEmpty,
           () => "-> ${constrainedRules.join(" ")}");
     }
@@ -147,10 +146,9 @@
     writeIf(chunk.indent != null && chunk.indent != 0,
         () => 'indent ${chunk.indent}');
 
-    writeIf(chunk.nesting != null && chunk.nesting.indent != 0,
-        () => 'nest ${chunk.nesting}');
+    writeIf(chunk.nesting.indent != 0, () => 'nest ${chunk.nesting}');
 
-    writeIf(chunk.flushLeft != null && chunk.flushLeft, () => 'flush');
+    writeIf(chunk.flushLeft, () => 'flush');
 
     writeIf(chunk.canDivide, () => 'divide');
 
@@ -193,8 +191,7 @@
 
 /// Shows all of the constraints between the rules used by [chunks].
 void dumpConstraints(List<Chunk> chunks) {
-  var rules =
-      chunks.map((chunk) => chunk.rule).where((rule) => rule != null).toSet();
+  var rules = chunks.map((chunk) => chunk.rule).whereType<Rule>().toSet();
 
   for (var rule in rules) {
     var constrainedValues = [];
diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart
index 8e8dcfe..86488a4 100644
--- a/lib/src/exceptions.dart
+++ b/lib/src/exceptions.dart
@@ -17,7 +17,7 @@
   const FormatterException(this.errors);
 
   /// Creates a human-friendly representation of the analysis errors.
-  String message({bool color}) {
+  String message({bool? color}) {
     var buffer = StringBuffer();
     buffer.writeln('Could not format because the source could not be parsed:');
 
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 893ad95..c0064b4 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -14,8 +14,8 @@
 import 'exceptions.dart';
 import 'source_code.dart';
 
-/// Reads input from stdin until it's closed, and the formats it.
-void formatStdin(FormatterOptions options, List<int> selection, String name) {
+/// Reads and formats input from stdin until closed.
+void formatStdin(FormatterOptions options, List<int>? selection, String name) {
   var selectionStart = 0;
   var selectionLength = 0;
 
@@ -129,7 +129,7 @@
 /// Runs the formatter on [file].
 ///
 /// Returns `true` if successful or `false` if an error occurred.
-bool processFile(FormatterOptions options, File file, {String displayPath}) {
+bool processFile(FormatterOptions options, File file, {String? displayPath}) {
   displayPath ??= file.path;
 
   var formatter = DartFormatter(
diff --git a/lib/src/line_splitting/line_splitter.dart b/lib/src/line_splitting/line_splitter.dart
index 137c21e..8ef5926 100644
--- a/lib/src/line_splitting/line_splitter.dart
+++ b/lib/src/line_splitting/line_splitter.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.src.line_splitting.line_splitter;
-
 import '../chunk.dart';
 import '../debug.dart' as debug;
 import '../line_writer.dart';
@@ -121,9 +118,6 @@
   /// and can stop looking.
   final _queue = SolveStateQueue();
 
-  /// The lowest cost solution found so far.
-  SolveState _bestSolution;
-
   /// Creates a new splitter for [_writer] that tries to fit [chunks] into the
   /// page width.
   LineSplitter(this.writer, List<Chunk> chunks, int blockIndentation,
@@ -133,7 +127,7 @@
         // Collect the set of rules that we need to select values for.
         rules = chunks
             .map((chunk) => chunk.rule)
-            .where((rule) => rule != null)
+            .whereType<Rule>()
             .toSet()
             .toList(growable: false),
         blockIndentation = blockIndentation,
@@ -165,20 +159,22 @@
     // Start with a completely unbound, unsplit solution.
     _queue.add(SolveState(this, RuleSet(rules.length)));
 
+    SolveState? bestSolution;
+
     var attempts = 0;
     while (_queue.isNotEmpty) {
       var state = _queue.removeFirst();
 
-      if (state.isBetterThan(_bestSolution)) {
-        _bestSolution = state;
+      if (state.isBetterThan(bestSolution)) {
+        bestSolution = state;
 
         // Since we sort solutions by cost the first solution we find that
         // fits is the winner.
-        if (_bestSolution.overflowChars == 0) break;
+        if (bestSolution.overflowChars == 0) break;
       }
 
       if (debug.traceSplitter) {
-        var best = state == _bestSolution ? ' (best)' : '';
+        var best = state == bestSolution ? ' (best)' : '';
         debug.log('$state$best');
         debug.dumpLines(chunks, firstLineIndent, state.splits);
         debug.log();
@@ -191,12 +187,12 @@
     }
 
     if (debug.traceSplitter) {
-      debug.log('$_bestSolution (winner)');
-      debug.dumpLines(chunks, firstLineIndent, _bestSolution.splits);
+      debug.log('$bestSolution (winner)');
+      debug.dumpLines(chunks, firstLineIndent, bestSolution!.splits);
       debug.log();
     }
 
-    return _bestSolution.splits;
+    return bestSolution!.splits;
   }
 
   void enqueue(SolveState state) {
diff --git a/lib/src/line_splitting/rule_set.dart b/lib/src/line_splitting/rule_set.dart
index 2d365ab..acffb83 100644
--- a/lib/src/line_splitting/rule_set.dart
+++ b/lib/src/line_splitting/rule_set.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.src.line_splitting.rule_set;
-
 import '../rule/rule.dart';
 
 /// An optimized data structure for storing a set of values for some rules.
@@ -16,7 +13,7 @@
 /// Internally, this then just stores the values in a sparse list whose indices
 /// are the indices of the rules.
 class RuleSet {
-  List<int> _values;
+  List<int?> _values;
 
   RuleSet(int numRules) : this._(List.filled(numRules, null));
 
@@ -27,7 +24,7 @@
     // Treat hardened rules as implicitly bound.
     if (rule.isHardened) return true;
 
-    return _values[rule.index] != null;
+    return _values[rule.index!] != null;
   }
 
   /// Gets the bound value for [rule] or [Rule.unsplit] if it is not bound.
@@ -35,7 +32,7 @@
     // Hardened rules are implicitly bound.
     if (rule.isHardened) return rule.fullySplitValue;
 
-    var value = _values[rule.index];
+    var value = _values[rule.index!];
     if (value != null) return value;
 
     return Rule.unsplit;
@@ -67,7 +64,7 @@
       List<Rule> rules, Rule rule, int value, void Function(Rule) onSplitRule) {
     assert(!rule.isHardened);
 
-    _values[rule.index] = value;
+    _values[rule.index!] = value;
 
     // Test this rule against the other rules being bound.
     for (var other in rule.constrainedRules) {
@@ -76,7 +73,7 @@
       if (other.isHardened) {
         otherValue = other.fullySplitValue;
       } else {
-        otherValue = _values[other.index];
+        otherValue = _values[other.index!];
       }
 
       var constraint = rule.constrain(value, other);
@@ -127,18 +124,17 @@
 /// chosen column is for the following line.
 ///
 /// Internally, this uses a list where each element corresponds to the column
-/// of the chunk at that index in the chunk list, or `null` if that chunk did
-/// not split. This had about a 10% perf improvement over using a [Set] of
-/// splits.
+/// of the chunk at that index in the chunk list, or `-1` if that chunk did not
+/// split. This had about a 10% perf improvement over using a [Set] of splits.
 class SplitSet {
   final List<int> _columns;
 
   /// The cost of the solution that led to these splits.
   int get cost => _cost;
-  int _cost;
+  late final int _cost;
 
   /// Creates a new empty split set for a line with [numChunks].
-  SplitSet(int numChunks) : _columns = List.filled(numChunks - 1, null);
+  SplitSet(int numChunks) : _columns = List.filled(numChunks - 1, -1);
 
   /// Marks the line after chunk [index] as starting at [column].
   void add(int index, int column) {
@@ -147,7 +143,7 @@
 
   /// Returns `true` if the chunk at [splitIndex] should be split.
   bool shouldSplitAt(int index) =>
-      index < _columns.length && _columns[index] != null;
+      index < _columns.length && _columns[index] != -1;
 
   /// Gets the zero-based starting column for the chunk at [index].
   int getColumn(int index) => _columns[index];
@@ -156,7 +152,6 @@
   ///
   /// This can only be called once.
   void setCost(int cost) {
-    assert(_cost == null);
     _cost = cost;
   }
 
@@ -164,7 +159,7 @@
   String toString() {
     var result = [];
     for (var i = 0; i < _columns.length; i++) {
-      if (_columns[i] != null) {
+      if (_columns[i] != -1) {
         result.add('$i:${_columns[i]}');
       }
     }
diff --git a/lib/src/line_splitting/solve_state.dart b/lib/src/line_splitting/solve_state.dart
index 8f202a2..708353e 100644
--- a/lib/src/line_splitting/solve_state.dart
+++ b/lib/src/line_splitting/solve_state.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.src.line_splitting.solve_state;
-
 import '../chunk.dart';
 import '../debug.dart' as debug;
 import '../nesting_level.dart';
@@ -31,12 +28,12 @@
   /// The set of [Rule]s that are bound by [_ruleValues].
   ///
   /// Cached by [_ensureConstraints] for use by [_ensureUnboundConstraints].
-  Set<Rule> _boundRules;
+  late Set<Rule> _boundRules;
 
   /// The set of [Rule]s that are not bound by [_ruleValues].
   ///
   /// Cached by [_ensureConstraints] for use by [_ensureUnboundConstraints].
-  Set<Rule> _unboundRules;
+  late Set<Rule> _unboundRules;
 
   /// The unbound rules in this state that can be bound to produce new more
   /// refined states.
@@ -68,12 +65,12 @@
 
   /// The set of splits chosen for this state.
   SplitSet get splits => _splits;
-  SplitSet _splits;
+  late final SplitSet _splits;
 
   /// The number of characters that do not fit inside the page with this set of
   /// splits.
   int get overflowChars => _overflowChars;
-  int _overflowChars;
+  int _overflowChars = 0;
 
   /// Whether we can treat this state as a complete solution by leaving its
   /// unbound rules unsplit.
@@ -86,7 +83,7 @@
 
   /// The constraints the bound rules in this state have on the remaining
   /// unbound rules.
-  Map<Rule, int> _constraints;
+  late final Map<Rule, int> _constraints = _initConstraints();
 
   /// The unbound rule values that are disallowed because they would place
   /// invalid constraints on the currently bound values.
@@ -99,14 +96,16 @@
   ///
   /// It's important to track this, because we can't allow to states to overlap
   /// if one permits more values for some unbound rule than the other does.
-  Map<Rule, Set<int>> _unboundConstraints;
+  late final Map<Rule, Set<int>> _unboundConstraints =
+      _initUnboundConstraints();
 
   /// The bound rules that appear inside lines also containing unbound rules.
   ///
   /// By appearing in the same line, it means these bound rules may affect the
   /// results of binding those unbound rules. This is used to tell if two
   /// states may diverge by binding unbound rules or not.
-  Set<Rule> _boundRulesInUnboundLines;
+  late final Set<Rule> _boundRulesInUnboundLines =
+      _initBoundRulesInUnboundLines();
 
   SolveState(this._splitter, this._ruleValues) {
     _calculateSplits();
@@ -119,7 +118,7 @@
 
   /// Returns `true` if this state is a better solution to use as the final
   /// result than [other].
-  bool isBetterThan(SolveState other) {
+  bool isBetterThan(SolveState? other) {
     // If this state contains an unbound rule that we know can't be left
     // unsplit, we can't pick this as a solution.
     if (!_isComplete) return false;
@@ -197,10 +196,10 @@
         for (var value = 1; value < rule.numValues; value++) {
           var boundRules = unsplitRules.clone();
 
-          List<Rule> mustSplitRules;
+          List<Rule>? mustSplitRules;
           var valid = boundRules.tryBind(_splitter.rules, rule, value, (rule) {
             mustSplitRules ??= [];
-            mustSplitRules.add(rule);
+            mustSplitRules!.add(rule);
           });
 
           // Make sure we don't violate the constraints of the bound rules.
@@ -211,7 +210,7 @@
           // If some unbound rules are constrained to split, remember that.
           if (mustSplitRules != null) {
             state._isComplete = false;
-            state._liveRules.addAll(mustSplitRules);
+            state._liveRules.addAll(mustSplitRules!);
           }
 
           _splitter.enqueue(state);
@@ -236,8 +235,6 @@
   bool _isOverlapping(SolveState other) {
     // Lines that contain both bound and unbound rules must have the same
     // bound values.
-    _ensureBoundRulesInUnboundLines();
-    other._ensureBoundRulesInUnboundLines();
     if (_boundRulesInUnboundLines.length !=
         other._boundRulesInUnboundLines.length) {
       return false;
@@ -250,23 +247,19 @@
       }
     }
 
-    _ensureConstraints();
-    other._ensureConstraints();
     if (_constraints.length != other._constraints.length) return false;
 
     for (var rule in _constraints.keys) {
       if (_constraints[rule] != other._constraints[rule]) return false;
     }
 
-    _ensureUnboundConstraints();
-    other._ensureUnboundConstraints();
     if (_unboundConstraints.length != other._unboundConstraints.length) {
       return false;
     }
 
     for (var rule in _unboundConstraints.keys) {
-      var disallowed = _unboundConstraints[rule];
-      var otherDisallowed = other._unboundConstraints[rule];
+      var disallowed = _unboundConstraints[rule]!;
+      var otherDisallowed = other._unboundConstraints[rule]!;
 
       if (disallowed.length != otherDisallowed.length) return false;
       for (var value in disallowed) {
@@ -285,7 +278,7 @@
     var usedNestingLevels = <NestingLevel>{};
     for (var i = 0; i < _splitter.chunks.length - 1; i++) {
       var chunk = _splitter.chunks[i];
-      if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) {
+      if (chunk.rule!.isSplit(getValue(chunk.rule!), chunk)) {
         usedNestingLevels.add(chunk.nesting);
         chunk.nesting.clearTotalUsedIndent();
       }
@@ -298,11 +291,11 @@
     _splits = SplitSet(_splitter.chunks.length);
     for (var i = 0; i < _splitter.chunks.length - 1; i++) {
       var chunk = _splitter.chunks[i];
-      if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) {
+      if (chunk.rule!.isSplit(getValue(chunk.rule!), chunk)) {
         var indent = 0;
         if (!chunk.flushLeftAfter) {
           // Add in the chunk's indent.
-          indent = _splitter.blockIndentation + chunk.indent;
+          indent = _splitter.blockIndentation + chunk.indent!;
 
           // And any expression nesting.
           indent += chunk.nesting.totalUsedIndent;
@@ -318,13 +311,9 @@
   /// Evaluates the cost (i.e. the relative "badness") of splitting the line
   /// into [lines] physical lines based on the current set of rules.
   void _calculateCost() {
-    assert(_splits != null);
-
     // Calculate the length of each line and apply the cost of any spans that
     // get split.
     var cost = 0;
-    _overflowChars = 0;
-
     var length = _splitter.firstLineIndent;
 
     // The unbound rules in use by the current line. This will be null after
@@ -435,7 +424,7 @@
   ///
   /// Only does this if [rule] is a valid soft rule. Returns `true` if any new
   /// live rules were added.
-  bool _addLiveRules(Rule rule) {
+  bool _addLiveRules(Rule? rule) {
     if (rule == null) return false;
 
     var added = false;
@@ -449,22 +438,19 @@
     return added;
   }
 
-  /// Lazily initializes the [_boundInUnboundLines], which is needed to compare
+  /// Used to lazy initialize [_boundInUnboundLines], which is needed to compare
   /// two states for overlap.
   ///
   /// We do this lazily because the calculation is a bit slow, and is only
   /// needed when we have two states with the same score.
-  void _ensureBoundRulesInUnboundLines() {
-    if (_boundRulesInUnboundLines != null) return;
-
-    _boundRulesInUnboundLines = <Rule>{};
-
+  Set<Rule> _initBoundRulesInUnboundLines() {
+    var rules = <Rule>{};
     var boundInLine = <Rule>{};
     var hasUnbound = false;
 
     for (var i = 0; i < _splitter.chunks.length - 1; i++) {
       if (splits.shouldSplitAt(i)) {
-        if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine);
+        if (hasUnbound) rules.addAll(boundInLine);
 
         boundInLine.clear();
         hasUnbound = false;
@@ -480,17 +466,16 @@
       }
     }
 
-    if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine);
+    if (hasUnbound) rules.addAll(boundInLine);
+    return rules;
   }
 
-  /// Lazily initializes the [_constraints], which is needed to compare two
-  /// states for overlap.
+  /// Used to lazy initializes the [_constraints], which is needed to compare
+  /// two states for overlap.
   ///
   /// We do this lazily because the calculation is a bit slow, and is only
   /// needed when we have two states with the same score.
-  void _ensureConstraints() {
-    if (_constraints != null) return;
-
+  Map<Rule, int> _initConstraints() {
     _unboundRules = <Rule>{};
     _boundRules = <Rule>{};
 
@@ -502,7 +487,7 @@
       }
     }
 
-    _constraints = {};
+    var constraints = <Rule, int>{};
 
     for (var bound in _boundRules) {
       for (var unbound in bound.constrainedRules) {
@@ -511,28 +496,24 @@
         var value = _ruleValues.getValue(bound);
         var constraint = bound.constrain(value, unbound);
         if (constraint != null) {
-          _constraints[unbound] = constraint;
+          constraints[unbound] = constraint;
         }
       }
     }
+
+    return constraints;
   }
 
-  /// Lazily initializes the [_unboundConstraints], which is needed to compare
-  /// two states for overlap.
+  /// Used to lazy initialize the [_unboundConstraints], which is needed to
+  /// compare two states for overlap.
   ///
   /// We do this lazily because the calculation is a bit slow, and is only
   /// needed when we have two states with the same score.
-  void _ensureUnboundConstraints() {
-    if (_unboundConstraints != null) return;
-
-    // _ensureConstraints should be called first which initializes these.
-    assert(_boundRules != null);
-    assert(_unboundRules != null);
-
-    _unboundConstraints = {};
-
+  Map<Rule, Set<int>> _initUnboundConstraints() {
+    var unboundConstraints = <Rule, Set<int>>{};
     for (var unbound in _unboundRules) {
-      Set<int> disallowedValues;
+      // Lazily create and add the set to the constraints only if needed.
+      late final disallowedValues = unboundConstraints[unbound] = {};
 
       for (var bound in unbound.constrainedRules) {
         if (!_boundRules.contains(bound)) continue;
@@ -555,15 +536,12 @@
             continue;
           }
 
-          if (disallowedValues == null) {
-            disallowedValues = <int>{};
-            _unboundConstraints[unbound] = disallowedValues;
-          }
-
           disallowedValues.add(value);
         }
       }
     }
+
+    return unboundConstraints;
   }
 
   @override
@@ -592,7 +570,6 @@
 
     if (overflowChars > 0) buffer.write(' (${overflowChars} over)');
     if (!_isComplete) buffer.write(' (incomplete)');
-    if (splits == null) buffer.write(' invalid');
 
     return buffer.toString();
   }
diff --git a/lib/src/line_splitting/solve_state_queue.dart b/lib/src/line_splitting/solve_state_queue.dart
index a299e15..c8efdbb 100644
--- a/lib/src/line_splitting/solve_state_queue.dart
+++ b/lib/src/line_splitting/solve_state_queue.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.src.line_splitting.solve_state_queue;
-
 import 'line_splitter.dart';
 import 'solve_state.dart';
 
@@ -22,10 +19,10 @@
   /// number of "tree levels" in the heap is only done for aesthetic reasons.
   static const int _initialCapacity = 7;
 
-  LineSplitter _splitter;
+  late final LineSplitter _splitter;
 
   /// List implementation of a heap.
-  List<SolveState> _queue = List<SolveState>.filled(_initialCapacity, null);
+  List<SolveState?> _queue = List.filled(_initialCapacity, null);
 
   /// Number of elements in queue.
   /// The heap is implemented in the first [_length] entries of [_queue].
@@ -34,9 +31,6 @@
   bool get isNotEmpty => _length != 0;
 
   void bindSplitter(LineSplitter splitter) {
-    // Only do this once.
-    assert(_splitter == null);
-
     _splitter = splitter;
   }
 
@@ -50,7 +44,7 @@
       var newCapacity = _queue.length * 2 + 1;
       if (newCapacity < _initialCapacity) newCapacity = _initialCapacity;
 
-      var newQueue = List<SolveState>.filled(newCapacity, null);
+      var newQueue = List<SolveState?>.filled(newCapacity, null);
       newQueue.setRange(0, _length, _queue);
       _queue = newQueue;
     }
@@ -62,12 +56,12 @@
     assert(_length > 0);
 
     // Remove the highest priority state.
-    var result = _queue[0];
+    var result = _queue[0]!;
     _length--;
 
     // Fill the gap with the one at the end of the list and re-heapify.
     if (_length > 0) {
-      var last = _queue[_length];
+      var last = _queue[_length]!;
       _queue[_length] = null;
       _bubbleDown(last, 0);
     }
@@ -135,7 +129,7 @@
     // also have lower priority.
     do {
       var index = position - 1;
-      var enqueued = _queue[index];
+      var enqueued = _queue[index]!;
 
       var comparison = _compareScore(enqueued, state);
 
@@ -185,7 +179,7 @@
   void _bubbleUp(SolveState element, int index) {
     while (index > 0) {
       var parentIndex = (index - 1) ~/ 2;
-      var parent = _queue[parentIndex];
+      var parent = _queue[parentIndex]!;
 
       if (_compare(element, parent) > 0) break;
 
@@ -205,8 +199,8 @@
 
     while (rightChildIndex < _length) {
       var leftChildIndex = rightChildIndex - 1;
-      var leftChild = _queue[leftChildIndex];
-      var rightChild = _queue[rightChildIndex];
+      var leftChild = _queue[leftChildIndex]!;
+      var rightChild = _queue[rightChildIndex]!;
 
       var comparison = _compare(leftChild, rightChild);
       var minChildIndex;
@@ -234,7 +228,7 @@
 
     var leftChildIndex = rightChildIndex - 1;
     if (leftChildIndex < _length) {
-      var child = _queue[leftChildIndex];
+      var child = _queue[leftChildIndex]!;
       var comparison = _compare(element, child);
 
       if (comparison > 0) {
diff --git a/lib/src/line_writer.dart b/lib/src/line_writer.dart
index 748bb9b..e8874a8 100644
--- a/lib/src/line_writer.dart
+++ b/lib/src/line_writer.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.line_writer;
-
 import 'chunk.dart';
 import 'dart_formatter.dart';
 import 'debug.dart' as debug;
@@ -35,19 +32,19 @@
   ///
   /// This will be `null` if there is no selection or the writer hasn't reached
   /// the beginning of the selection yet.
-  int _selectionStart;
+  int? _selectionStart;
 
   /// The offset in [_buffer] where the selection ends in the formatted code.
   ///
   /// This will be `null` if there is no selection or the writer hasn't reached
   /// the end of the selection yet.
-  int _selectionEnd;
+  int? _selectionEnd;
 
   /// The number of characters that have been written to the output.
   int get length => _buffer.length;
 
   LineWriter(DartFormatter formatter, this._chunks)
-      : _lineEnding = formatter.lineEnding,
+      : _lineEnding = formatter.lineEnding!,
         pageWidth = formatter.pageWidth,
         _blockIndentation = 0,
         _blockCache = {};
@@ -114,7 +111,7 @@
 
       // Get ready for the next line.
       newlines = chunk.isDouble ? 2 : 1;
-      indent = chunk.indent;
+      indent = chunk.indent!;
       flushLeft = chunk.flushLeft;
       start = i + 1;
     }
@@ -134,7 +131,7 @@
   /// Takes the chunks from [start] to [end] with leading [indent], removes
   /// them, and runs the [LineSplitter] on them.
   int _completeLine(int newlines, int indent, int start, int end,
-      {bool flushLeft}) {
+      {required bool flushLeft}) {
     // Write the newlines required by the previous line.
     for (var j = 0; j < newlines; j++) {
       _buffer.write(_lineEnding);
@@ -175,11 +172,11 @@
           // If this block contains one of the selection markers, tell the
           // writer where it ended up in the final output.
           if (block.selectionStart != null) {
-            _selectionStart = length + block.selectionStart;
+            _selectionStart = length + block.selectionStart!;
           }
 
           if (block.selectionEnd != null) {
-            _selectionEnd = length + block.selectionEnd;
+            _selectionEnd = length + block.selectionEnd!;
           }
 
           _buffer.write(block.text);
@@ -220,11 +217,11 @@
   /// contains a selection marker.
   void _writeChunk(Chunk chunk) {
     if (chunk.selectionStart != null) {
-      _selectionStart = length + chunk.selectionStart;
+      _selectionStart = length + chunk.selectionStart!;
     }
 
     if (chunk.selectionEnd != null) {
-      _selectionEnd = length + chunk.selectionEnd;
+      _selectionEnd = length + chunk.selectionEnd!;
     }
 
     _buffer.write(chunk.text);
@@ -269,13 +266,13 @@
   /// if it was contained within this split list of chunks.
   ///
   /// Otherwise, this is `null`.
-  final int selectionStart;
+  final int? selectionStart;
 
   /// Where in the resulting buffer the selection end point should appear if it
   /// was contained within this split list of chunks.
   ///
   /// Otherwise, this is `null`.
-  final int selectionEnd;
+  final int? selectionEnd;
 
   FormatResult(this.text, this.cost, this.selectionStart, this.selectionEnd);
 }
diff --git a/lib/src/nesting_builder.dart b/lib/src/nesting_builder.dart
index 8ed62f1..4740114 100644
--- a/lib/src/nesting_builder.dart
+++ b/lib/src/nesting_builder.dart
@@ -51,7 +51,7 @@
   ///
   /// Here, if we discard the expression nesting before we reach the "{", then
   /// it won't get indented as it should.
-  NestingLevel _pendingNesting;
+  NestingLevel? _pendingNesting;
 
   /// The current number of characters of block indentation.
   int get indentation => _stack.last;
@@ -66,7 +66,7 @@
   /// Creates a new indentation level [spaces] deeper than the current one.
   ///
   /// If omitted, [spaces] defaults to [Indent.block].
-  void indent([int spaces]) {
+  void indent([int? spaces]) {
     spaces ??= Indent.block;
 
     // Indentation should only change outside of nesting.
@@ -95,11 +95,11 @@
   /// if the previous line has a lower level of nesting.
   ///
   /// If [indent] is omitted, defaults to [Indent.expression].
-  void nest([int indent]) {
+  void nest([int? indent]) {
     indent ??= Indent.expression;
 
     if (_pendingNesting != null) {
-      _pendingNesting = _pendingNesting.nest(indent);
+      _pendingNesting = _pendingNesting!.nest(indent);
     } else {
       _pendingNesting = _nesting.nest(indent);
     }
@@ -108,7 +108,7 @@
   /// Discards the most recent level of expression nesting.
   void unnest() {
     if (_pendingNesting != null) {
-      _pendingNesting = _pendingNesting.parent;
+      _pendingNesting = _pendingNesting!.parent;
     } else {
       _pendingNesting = _nesting.parent;
     }
@@ -121,7 +121,7 @@
   void commitNesting() {
     if (_pendingNesting == null) return;
 
-    _nesting = _pendingNesting;
+    _nesting = _pendingNesting!;
     _pendingNesting = null;
   }
 }
diff --git a/lib/src/nesting_level.dart b/lib/src/nesting_level.dart
index c1088c6..acbdfe3 100644
--- a/lib/src/nesting_level.dart
+++ b/lib/src/nesting_level.dart
@@ -24,8 +24,8 @@
 class NestingLevel extends FastHash {
   /// The nesting level surrounding this one, or `null` if this is represents
   /// top level code in a block.
-  NestingLevel get parent => _parent;
-  NestingLevel _parent;
+  NestingLevel? get parent => _parent;
+  NestingLevel? _parent;
 
   /// The number of characters that this nesting level is indented relative to
   /// the containing level.
@@ -37,8 +37,8 @@
   /// its parents, after determining which nesting levels are actually used.
   ///
   /// This is only valid during line splitting.
-  int get totalUsedIndent => _totalUsedIndent;
-  int _totalUsedIndent;
+  int get totalUsedIndent => _totalUsedIndent!;
+  int? _totalUsedIndent;
 
   bool get isNested => _parent != null;
 
@@ -53,22 +53,25 @@
   /// Clears the previously calculated total indent of this nesting level.
   void clearTotalUsedIndent() {
     _totalUsedIndent = null;
-    if (_parent != null) _parent.clearTotalUsedIndent();
+    _parent?.clearTotalUsedIndent();
   }
 
   /// Calculates the total amount of indentation from this nesting level and
   /// all of its parents assuming only [usedNesting] levels are in use.
   void refreshTotalUsedIndent(Set<NestingLevel> usedNesting) {
-    if (_totalUsedIndent != null) return;
+    var totalIndent = _totalUsedIndent;
+    if (totalIndent != null) return;
 
-    _totalUsedIndent = 0;
+    totalIndent = 0;
 
     if (_parent != null) {
-      _parent.refreshTotalUsedIndent(usedNesting);
-      _totalUsedIndent += _parent.totalUsedIndent;
+      _parent!.refreshTotalUsedIndent(usedNesting);
+      totalIndent += _parent!.totalUsedIndent;
     }
 
-    if (usedNesting.contains(this)) _totalUsedIndent += indent;
+    if (usedNesting.contains(this)) totalIndent += indent;
+
+    _totalUsedIndent = totalIndent;
   }
 
   @override
diff --git a/lib/src/rule/argument.dart b/lib/src/rule/argument.dart
index a483e4e..7ff8378 100644
--- a/lib/src/rule/argument.dart
+++ b/lib/src/rule/argument.dart
@@ -1,19 +1,16 @@
 // Copyright (c) 2015, 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 dart_style.src.rule.argument;
-
 import '../chunk.dart';
 import 'rule.dart';
 
 /// Base class for a rule that handles argument or parameter lists.
 abstract class ArgumentRule extends Rule {
   /// The chunks prior to each positional argument.
-  final List<Chunk> _arguments = [];
+  final List<Chunk?> _arguments = [];
 
   /// The rule used to split collections in the argument list, if any.
-  Rule _collectionRule;
+  Rule? _collectionRule;
 
   /// The number of leading collection arguments.
   ///
@@ -43,20 +40,18 @@
   @override
   void addConstrainedRules(Set<Rule> rules) {
     super.addConstrainedRules(rules);
-    if (_collectionRule != null) rules.add(_collectionRule);
+    if (_collectionRule != null) rules.add(_collectionRule!);
   }
 
   @override
   void forgetUnusedRules() {
     super.forgetUnusedRules();
-    if (_collectionRule != null && _collectionRule.index == null) {
-      _collectionRule = null;
-    }
+    if (_collectionRule?.index == null) _collectionRule = null;
   }
 
   /// Remembers [chunk] as containing the split that occurs right before an
   /// argument in the list.
-  void beforeArgument(Chunk chunk) {
+  void beforeArgument(Chunk? chunk) {
     _arguments.add(chunk);
   }
 
@@ -93,14 +88,14 @@
 class PositionalRule extends ArgumentRule {
   /// If there are named arguments following these positional ones, this will
   /// be their rule.
-  Rule _namedArgsRule;
+  Rule? _namedArgsRule;
 
   /// Creates a new rule for a positional argument list.
   ///
   /// If [_collectionRule] is given, it is the rule used to split the collection
   /// arguments in the list.
   PositionalRule(
-      Rule collectionRule, int leadingCollections, int trailingCollections)
+      Rule? collectionRule, int leadingCollections, int trailingCollections)
       : super(collectionRule, leadingCollections, trailingCollections);
 
   @override
@@ -126,15 +121,13 @@
   @override
   void addConstrainedRules(Set<Rule> rules) {
     super.addConstrainedRules(rules);
-    if (_namedArgsRule != null) rules.add(_namedArgsRule);
+    if (_namedArgsRule != null) rules.add(_namedArgsRule!);
   }
 
   @override
   void forgetUnusedRules() {
     super.forgetUnusedRules();
-    if (_namedArgsRule != null && _namedArgsRule.index == null) {
-      _namedArgsRule = null;
-    }
+    if (_namedArgsRule?.index == null) _namedArgsRule = null;
   }
 
   @override
@@ -186,14 +179,14 @@
   ///          argument,
   ///          argument, named: argument);
   @override
-  int constrain(int value, Rule other) {
+  int? constrain(int value, Rule other) {
     var constrained = super.constrain(value, other);
     if (constrained != null) return constrained;
 
     // Handle the relationship between the positional and named args.
     if (other == _namedArgsRule) {
       // If the positional args are one-per-line, the named args are too.
-      if (value == fullySplitValue) return _namedArgsRule.fullySplitValue;
+      if (value == fullySplitValue) return _namedArgsRule!.fullySplitValue;
 
       // Otherwise, if there is any split in the positional arguments, don't
       // allow the named arguments on the same line as them.
@@ -257,7 +250,7 @@
   int get numValues => 3;
 
   NamedRule(
-      Rule collectionRule, int leadingCollections, int trailingCollections)
+      Rule? collectionRule, int leadingCollections, int trailingCollections)
       : super(collectionRule, leadingCollections, trailingCollections);
 
   @override
@@ -270,7 +263,7 @@
   }
 
   @override
-  int constrain(int value, Rule other) {
+  int? constrain(int value, Rule other) {
     var constrained = super.constrain(value, other);
     if (constrained != null) return constrained;
 
diff --git a/lib/src/rule/metadata.dart b/lib/src/rule/metadata.dart
index c552101..3d59e96 100644
--- a/lib/src/rule/metadata.dart
+++ b/lib/src/rule/metadata.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2015, 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 dart_style.src.rule.metadata;
-
 import 'argument.dart';
 import 'rule.dart';
 
@@ -17,8 +14,8 @@
 /// Also, if the annotations split, we force the entire parameter list to fully
 /// split, both named and positional.
 class MetadataRule extends Rule {
-  Rule _positionalRule;
-  Rule _namedRule;
+  Rule? _positionalRule;
+  Rule? _namedRule;
 
   /// Remembers that [rule] is the [PositionalRule] used by the argument list
   /// containing the parameter metadata using this rule.
@@ -35,7 +32,7 @@
   /// Constrains the surrounding argument list rules to fully split if the
   /// metadata does.
   @override
-  int constrain(int value, Rule other) {
+  int? constrain(int value, Rule other) {
     var constrained = super.constrain(value, other);
     if (constrained != null) return constrained;
 
@@ -43,27 +40,22 @@
     if (value == Rule.unsplit) return null;
 
     // Otherwise, they have to split.
-    if (other == _positionalRule) return _positionalRule.fullySplitValue;
-    if (other == _namedRule) return _namedRule.fullySplitValue;
+    if (other == _positionalRule) return _positionalRule!.fullySplitValue;
+    if (other == _namedRule) return _namedRule!.fullySplitValue;
 
     return null;
   }
 
   @override
   void addConstrainedRules(Set<Rule> rules) {
-    if (_positionalRule != null) rules.add(_positionalRule);
-    if (_namedRule != null) rules.add(_namedRule);
+    if (_positionalRule != null) rules.add(_positionalRule!);
+    if (_namedRule != null) rules.add(_namedRule!);
   }
 
   @override
   void forgetUnusedRules() {
     super.forgetUnusedRules();
-    if (_positionalRule != null && _positionalRule.index == null) {
-      _positionalRule = null;
-    }
-
-    if (_namedRule != null && _namedRule.index == null) {
-      _namedRule = null;
-    }
+    if (_positionalRule?.index == null) _positionalRule = null;
+    if (_namedRule?.index == null) _namedRule = null;
   }
 }
diff --git a/lib/src/rule/rule.dart b/lib/src/rule/rule.dart
index 7813bf1..660b8d0 100644
--- a/lib/src/rule/rule.dart
+++ b/lib/src/rule/rule.dart
@@ -2,8 +2,6 @@
 // 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 dart_style.src.rule.rule;
-
 import '../chunk.dart';
 import '../fast_hash.dart';
 
@@ -40,7 +38,7 @@
 
   /// During line splitting [LineSplitter] sets this to the index of this
   /// rule in its list of rules.
-  int index;
+  int? index;
 
   /// If `true`, the rule has been "hardened" meaning it's been placed into a
   /// permanent "must fully split" state.
@@ -75,7 +73,7 @@
   /// rules.
   bool get splitsOnInnerRules => true;
 
-  Rule([int cost]) : _cost = cost ?? Cost.normal;
+  Rule([int? cost]) : _cost = cost ?? Cost.normal;
 
   /// Creates a new rule that is already fully split.
   Rule.hard() : _cost = 0 {
@@ -114,8 +112,8 @@
   /// Allows relationships between rules like "if I split, then this should
   /// split too". Returns a non-negative value to force [other] to take that
   /// value. Returns -1 to allow [other] to take any non-zero value. Returns
-  /// null to not constrain other.
-  int constrain(int value, Rule other) {
+  /// `null` to not constrain other.
+  int? constrain(int value, Rule other) {
     // By default, any containing rule will be fully split if this one is split.
     if (value == Rule.unsplit) return null;
     if (_implied.contains(other)) return other.fullySplitValue;
@@ -151,35 +149,40 @@
     // Lazy initialize this on first use. Note: Assumes this is only called
     // after the chunks have been written and any constraints have been wired
     // up.
-    if (_constrainedRules == null) {
-      _constrainedRules = _implied.toSet();
-      addConstrainedRules(_constrainedRules);
-    }
+    var rules = _constrainedRules;
+    if (rules != null) return rules;
 
-    return _constrainedRules;
+    rules = _implied.toSet();
+    addConstrainedRules(rules);
+    _constrainedRules = rules;
+    return rules;
   }
 
-  Set<Rule> _constrainedRules;
+  Set<Rule>? _constrainedRules;
 
   /// The transitive closure of all of the rules this rule places constraints
   /// on, directly or indirectly, including itself.
   Set<Rule> get allConstrainedRules {
-    if (_allConstrainedRules == null) {
-      void visit(Rule rule) {
-        if (_allConstrainedRules.contains(rule)) return;
+    var rules = _allConstrainedRules;
+    if (rules != null) return rules;
 
-        _allConstrainedRules.add(rule);
-        rule.constrainedRules.forEach(visit);
-      }
-
-      _allConstrainedRules = {};
-      visit(this);
-    }
-
-    return _allConstrainedRules;
+    rules = {};
+    _traverseConstraints(rules, this);
+    _allConstrainedRules = rules;
+    return rules;
   }
 
-  Set<Rule> _allConstrainedRules;
+  /// Traverses the constraint graph of [rule] adding everything to [rules].
+  void _traverseConstraints(Set<Rule> rules, Rule rule) {
+    if (rules.contains(rule)) return;
+
+    rules.add(rule);
+    for (var rule in rule.constrainedRules) {
+      _traverseConstraints(rules, rule);
+    }
+  }
+
+  Set<Rule>? _allConstrainedRules;
 
   @override
   String toString() => '$id';
diff --git a/lib/src/source_code.dart b/lib/src/source_code.dart
index 949daff..87c5aa5 100644
--- a/lib/src/source_code.dart
+++ b/lib/src/source_code.dart
@@ -10,7 +10,7 @@
   /// The [uri] where the source code is from.
   ///
   /// Used in error messages if the code cannot be parsed.
-  final String uri;
+  final String? uri;
 
   /// The Dart source code text.
   final String text;
@@ -20,10 +20,10 @@
 
   /// The offset in [text] where the selection begins, or `null` if there is
   /// no selection.
-  final int selectionStart;
+  final int? selectionStart;
 
   /// The number of selected characters or `null` if there is no selection.
-  final int selectionLength;
+  final int? selectionLength;
 
   /// Gets the source code before the beginning of the selection.
   ///
@@ -38,7 +38,7 @@
   /// If there is no selection, returns an empty string.
   String get selectedText {
     if (selectionStart == null) return '';
-    return text.substring(selectionStart, selectionStart + selectionLength);
+    return text.substring(selectionStart!, selectionStart! + selectionLength!);
   }
 
   /// Gets the source code following the selection.
@@ -46,7 +46,7 @@
   /// If there is no selection, returns an empty string.
   String get textAfterSelection {
     if (selectionStart == null) return '';
-    return text.substring(selectionStart + selectionLength);
+    return text.substring(selectionStart! + selectionLength!);
   }
 
   SourceCode(this.text,
@@ -61,21 +61,21 @@
     }
 
     if (selectionStart != null) {
-      if (selectionStart < 0) {
+      if (selectionStart! < 0) {
         throw ArgumentError('selectionStart must be non-negative.');
       }
 
-      if (selectionStart > text.length) {
+      if (selectionStart! > text.length) {
         throw ArgumentError('selectionStart must be within text.');
       }
     }
 
     if (selectionLength != null) {
-      if (selectionLength < 0) {
+      if (selectionLength! < 0) {
         throw ArgumentError('selectionLength must be non-negative.');
       }
 
-      if (selectionStart + selectionLength > text.length) {
+      if (selectionStart! + selectionLength! > text.length) {
         throw ArgumentError('selectionLength must end within text.');
       }
     }
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index ebbae38..c7bc88c 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -1,9 +1,6 @@
 // Copyright (c) 2014, 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 dart_style.src.source_visitor;
-
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/standard_ast_factory.dart';
 import 'package:analyzer/dart/ast/token.dart';
@@ -52,14 +49,13 @@
   /// splitting up named constructors.
   static bool looksLikeStaticCall(Expression node) {
     if (node is! MethodInvocation) return false;
-    var invocation = node as MethodInvocation;
-    if (invocation.target == null) return false;
+    if (node.target == null) return false;
 
     // A prefixed unnamed constructor call:
     //
     //     prefix.Foo();
-    if (invocation.target is SimpleIdentifier &&
-        _looksLikeClassName(invocation.methodName.name)) {
+    if (node.target is SimpleIdentifier &&
+        _looksLikeClassName(node.methodName.name)) {
       return true;
     }
 
@@ -67,10 +63,8 @@
     //
     //     Foo.named();
     //     prefix.Foo.named();
-    var target = invocation.target;
-    if (target is PrefixedIdentifier) {
-      target = (target as PrefixedIdentifier).identifier;
-    }
+    var target = node.target;
+    if (target is PrefixedIdentifier) target = target.identifier;
 
     return target is SimpleIdentifier && _looksLikeClassName(target.name);
   }
@@ -142,7 +136,7 @@
   /// The character offset of the end of the selection, if there is a selection.
   ///
   /// This is calculated and cached by [_findSelectionEnd].
-  int _selectionEnd;
+  int? _selectionEnd;
 
   /// How many levels deep inside a constant context the visitor currently is.
   int _constNesting = 0;
@@ -200,9 +194,8 @@
 
   /// Initialize a newly created visitor to write source code representing
   /// the visited nodes to the given [writer].
-  SourceVisitor(this._formatter, this._lineInfo, this._source) {
-    builder = ChunkBuilder(_formatter, _source);
-  }
+  SourceVisitor(this._formatter, this._lineInfo, this._source)
+      : builder = ChunkBuilder(_formatter, _source);
 
   /// Runs the visitor on [node], formatting its contents.
   ///
@@ -215,7 +208,7 @@
     visit(node);
 
     // Output trailing comments.
-    writePrecedingCommentsAndNewlines(node.endToken.next);
+    writePrecedingCommentsAndNewlines(node.endToken.next!);
 
     assert(_constNesting == 0, 'Should have exited all const contexts.');
 
@@ -306,7 +299,7 @@
     if (node.arguments != null) {
       // Metadata annotations are always const contexts.
       _constNesting++;
-      visitArgumentList(node.arguments, nestExpression: false);
+      visitArgumentList(node.arguments!, nestExpression: false);
       _constNesting--;
     }
 
@@ -369,7 +362,7 @@
     token(node.assertKeyword);
 
     var arguments = <Expression>[node.condition];
-    if (node.message != null) arguments.add(node.message);
+    if (node.message != null) arguments.add(node.message!);
 
     // If the argument list has a trailing comma, format it like a collection
     // literal where each argument goes on its own line, they are indented +2,
@@ -393,7 +386,7 @@
       token(node.assertKeyword);
 
       var arguments = [node.condition];
-      if (node.message != null) arguments.add(node.message);
+      if (node.message != null) arguments.add(node.message!);
 
       // If the argument list has a trailing comma, format it like a collection
       // literal where each argument goes on its own line, they are indented +2,
@@ -648,7 +641,7 @@
 
     // If the target is a call with a trailing comma in the argument list,
     // treat it like a collection literal.
-    ArgumentList arguments;
+    ArgumentList? arguments;
     if (expression is InvocationExpression) {
       arguments = expression.argumentList;
     } else if (expression is InstanceCreationExpression) {
@@ -907,7 +900,7 @@
     // the parameter list gets more deeply indented.
     if (node.redirectedConstructor != null) builder.nestExpression();
 
-    _visitBody(null, node.parameters, node.body, () {
+    _visitBody(null, node.parameters, node.body!, () {
       // Check for redirects or initializer lists.
       if (node.redirectedConstructor != null) {
         _visitConstructorRedirects(node);
@@ -945,7 +938,7 @@
       space();
       if (node.initializers.length > 1) {
         _writeText(node.parameters.parameters.last.isOptional ? ' ' : '  ',
-            node.separator.offset);
+            node.separator!.offset);
       }
 
       // ":".
@@ -1044,15 +1037,15 @@
       if (_formatter.fixes.contains(StyleFix.namedDefaultSeparator)) {
         // Change the separator to "=".
         space();
-        writePrecedingCommentsAndNewlines(node.separator);
-        _writeText('=', node.separator.offset);
+        writePrecedingCommentsAndNewlines(node.separator!);
+        _writeText('=', node.separator!.offset);
       } else {
         // The '=' separator is preceded by a space, ":" is not.
-        if (node.separator.type == TokenType.EQ) space();
+        if (node.separator!.type == TokenType.EQ) space();
         token(node.separator);
       }
 
-      soloSplit(_assignmentCost(node.defaultValue));
+      soloSplit(_assignmentCost(node.defaultValue!));
       visit(node.defaultValue);
 
       builder.unnest();
@@ -1204,7 +1197,7 @@
     if (expression is PropertyAccess) {
       return expression.realTarget;
     } else if (expression is MethodInvocation) {
-      return expression.realTarget;
+      return expression.realTarget!;
     } else if (expression is IndexExpression) {
       return expression.realTarget;
     }
@@ -1234,7 +1227,7 @@
           _insertCascadeTargetIntoExpression(expressionTarget, cascadeTarget),
           // If we've reached the end, replace the `..` operator with `.`
           expressionTarget == cascadeTarget
-              ? _synthesizeToken(TokenType.PERIOD, expression.operator)
+              ? _synthesizeToken(TokenType.PERIOD, expression.operator!)
               : expression.operator,
           expression.methodName,
           expression.typeArguments,
@@ -1244,9 +1237,8 @@
 
       // A null-aware cascade treats the `?` in `?..` as part of the token, but
       // for a non-cascade index, it is a separate `?` token.
-      if (expression.period != null &&
-          expression.period.type == TokenType.QUESTION_PERIOD_PERIOD) {
-        question = _synthesizeToken(TokenType.QUESTION, expression.period);
+      if (expression.period?.type == TokenType.QUESTION_PERIOD_PERIOD) {
+        question = _synthesizeToken(TokenType.QUESTION, expression.period!);
       }
 
       return astFactory.indexExpressionForTarget2(
@@ -1264,7 +1256,7 @@
   /// Parenthesize the target of the given statement's expression (assumed to
   /// be a CascadeExpression) before removing the cascade.
   void _fixCascadeByParenthesizingTarget(ExpressionStatement statement) {
-    CascadeExpression cascade = statement.expression;
+    var cascade = statement.expression as CascadeExpression;
     assert(cascade.cascadeSections.length == 1);
 
     // Write any leading comments and whitespace immediately, as they should
@@ -1736,7 +1728,7 @@
     var oldConstNesting = _constNesting;
     _constNesting = 0;
 
-    _visitBody(node.typeParameters, node.parameters, node.body);
+    _visitBody(node.typeParameters, node.parameters, node.body!);
 
     _constNesting = oldConstNesting;
   }
@@ -1841,10 +1833,10 @@
     // Treat a chain of if-else elements as a single unit so that we don't
     // unnecessarily indent each subsequent section of the chain.
     var ifElements = [
-      for (CollectionElement thisNode = node;
+      for (CollectionElement? thisNode = node;
           thisNode is IfElement;
-          thisNode = (thisNode as IfElement).elseElement)
-        thisNode as IfElement
+          thisNode = thisNode.elseElement)
+        thisNode
     ];
 
     // If the body of the then or else branch is a spread of a collection
@@ -1884,7 +1876,7 @@
     var elseSpreadBracket =
         _findSpreadCollectionBracket(ifElements.last.elseElement);
     if (elseSpreadBracket != null) {
-      spreadBrackets[ifElements.last.elseElement] = elseSpreadBracket;
+      spreadBrackets[ifElements.last.elseElement!] = elseSpreadBracket;
       beforeBlock(elseSpreadBracket, spreadRule, null);
     }
 
@@ -2012,7 +2004,7 @@
       }
 
       token(node.elseKeyword);
-      visitClause(node.elseStatement);
+      visitClause(node.elseStatement!);
     }
   }
 
@@ -2089,10 +2081,10 @@
     var includeKeyword = true;
 
     if (node.keyword != null) {
-      if (node.keyword.keyword == Keyword.NEW &&
+      if (node.keyword!.keyword == Keyword.NEW &&
           _formatter.fixes.contains(StyleFix.optionalNew)) {
         includeKeyword = false;
-      } else if (node.keyword.keyword == Keyword.CONST &&
+      } else if (node.keyword!.keyword == Keyword.CONST &&
           _formatter.fixes.contains(StyleFix.optionalConst) &&
           _constNesting > 0) {
         includeKeyword = false;
@@ -2103,7 +2095,7 @@
       token(node.keyword, after: space);
     } else {
       // Don't lose comments before the discarded keyword, if any.
-      writePrecedingCommentsAndNewlines(node.keyword);
+      writePrecedingCommentsAndNewlines(node.keyword!);
     }
 
     builder.startSpan(Cost.constructorName);
@@ -2282,21 +2274,20 @@
 
     // If there is only a single superclass constraint, format it like an
     // "extends" in a class.
-    if (node.onClause != null &&
-        node.onClause.superclassConstraints.length == 1) {
+    var onClause = node.onClause;
+    if (onClause != null && onClause.superclassConstraints.length == 1) {
       soloSplit();
-      token(node.onClause.onKeyword);
+      token(onClause.onKeyword);
       space();
-      visit(node.onClause.superclassConstraints.single);
+      visit(onClause.superclassConstraints.single);
     }
 
     builder.startRule(CombinatorRule());
 
     // If there are multiple superclass constraints, format them like the
     // "implements" clause.
-    if (node.onClause != null &&
-        node.onClause.superclassConstraints.length > 1) {
-      visit(node.onClause);
+    if (onClause != null && onClause.superclassConstraints.length > 1) {
+      visit(onClause);
     }
 
     visit(node.implementsClause);
@@ -2471,11 +2462,11 @@
         // Parameters can use "var" instead of "dynamic". Since we are inserting
         // "dynamic" in that case, remove the "var".
         if (node.keyword != null) {
-          if (node.keyword.type != Keyword.VAR) {
+          if (node.keyword!.type != Keyword.VAR) {
             modifier(node.keyword);
           } else {
             // Keep any comment attached to "var".
-            writePrecedingCommentsAndNewlines(node.keyword);
+            writePrecedingCommentsAndNewlines(node.keyword!);
           }
         }
 
@@ -2484,8 +2475,8 @@
         // without a name. Add "dynamic" in that case.
 
         // Ensure comments on the identifier comes before the inserted type.
-        token(node.identifier.token, before: () {
-          _writeText('dynamic', node.identifier.offset);
+        token(node.identifier!.token, before: () {
+          _writeText('dynamic', node.identifier!.offset);
           split();
         });
       } else {
@@ -2599,7 +2590,7 @@
     var components = node.components;
     for (var component in components) {
       // The '.' separator
-      if (component.previous.lexeme == '.') {
+      if (component.previous!.lexeme == '.') {
         token(component.previous);
       }
       token(component);
@@ -2690,7 +2681,8 @@
     var hasMultipleVariables =
         (node.parent as VariableDeclarationList).variables.length > 1;
 
-    _visitAssignment(node.equals, node.initializer, nest: hasMultipleVariables);
+    _visitAssignment(node.equals!, node.initializer!,
+        nest: hasMultipleVariables);
   }
 
   @override
@@ -2764,7 +2756,7 @@
 
   /// Visit a [node], and if not null, optionally preceded or followed by the
   /// specified functions.
-  void visit(AstNode node, {void Function() before, void Function() after}) {
+  void visit(AstNode? node, {void Function()? before, void Function()? after}) {
     if (node == null) return;
 
     if (before != null) before();
@@ -2801,7 +2793,7 @@
   /// the parameter.
   void visitParameterMetadata(
       NodeList<Annotation> metadata, void Function() visitParameter) {
-    if (metadata == null || metadata.isEmpty) {
+    if (metadata.isEmpty) {
       visitParameter();
       return;
     }
@@ -2835,7 +2827,7 @@
   /// the surrounding named argument rule. That way, this can ensure that a
   /// split between the name and argument forces the argument list to split
   /// too.
-  void visitNamedArgument(NamedExpression node, [NamedRule rule]) {
+  void visitNamedArgument(NamedExpression node, [NamedRule? rule]) {
     builder.nestExpression();
     builder.startSpan();
     visit(node.name);
@@ -2887,7 +2879,7 @@
     builder.nestExpression();
 
     token(leftBracket);
-    rule.beforeArgument(zeroSplit());
+    rule.beforeArgument(zeroSplit()!);
 
     for (var node in nodes) {
       visit(node);
@@ -2895,7 +2887,7 @@
       // Write the trailing comma.
       if (node != nodes.last) {
         token(node.endToken.next);
-        rule.beforeArgument(split());
+        rule.beforeArgument(split()!);
       }
     }
 
@@ -2959,7 +2951,7 @@
     visit(node.name);
     builder.endSpan();
 
-    TypeParameterList typeParameters;
+    TypeParameterList? typeParameters;
     if (node is FunctionDeclaration) {
       typeParameters = node.functionExpression.typeParameters;
     } else {
@@ -2981,9 +2973,9 @@
   /// space before it if it's not empty.
   ///
   /// If [beforeBody] is provided, it is invoked before the body is visited.
-  void _visitBody(TypeParameterList typeParameters,
-      FormalParameterList parameters, FunctionBody body,
-      [void Function() beforeBody]) {
+  void _visitBody(TypeParameterList? typeParameters,
+      FormalParameterList? parameters, FunctionBody body,
+      [void Function()? beforeBody]) {
     // If the body is "=>", add an extra level of indentation around the
     // parameters and a rule that spans the parameters and the "=>". This
     // ensures that if the parameters wrap, they wrap more deeply than the "=>"
@@ -3025,7 +3017,7 @@
   /// Visits the type parameters (if any) and formal parameters of a method
   /// declaration, function declaration, or generic function type.
   void _visitParameterSignature(
-      TypeParameterList typeParameters, FormalParameterList parameters) {
+      TypeParameterList? typeParameters, FormalParameterList? parameters) {
     // Start the nesting for the parameters here, so they indent past the
     // type parameters too, if any.
     builder.nestExpression();
@@ -3064,10 +3056,10 @@
   /// Visit a list of [nodes] if not null, optionally separated and/or preceded
   /// and followed by the given functions.
   void visitNodes(Iterable<AstNode> nodes,
-      {void Function() before,
-      void Function() between,
-      void Function() after}) {
-    if (nodes == null || nodes.isEmpty) return;
+      {void Function()? before,
+      void Function()? between,
+      void Function()? after}) {
+    if (nodes.isEmpty) return;
 
     if (before != null) before();
 
@@ -3082,8 +3074,8 @@
 
   /// Visit a comma-separated list of [nodes] if not null.
   void visitCommaSeparatedNodes(Iterable<AstNode> nodes,
-      {void Function() between}) {
-    if (nodes == null || nodes.isEmpty) return;
+      {void Function()? between}) {
+    if (nodes.isEmpty) return;
 
     between ??= space;
 
@@ -3095,7 +3087,7 @@
       visit(node);
 
       // The comma after the node.
-      if (node.endToken.next.lexeme == ',') token(node.endToken.next);
+      if (node.endToken.next!.lexeme == ',') token(node.endToken.next);
     }
   }
 
@@ -3104,16 +3096,16 @@
   ///
   /// This is also used for argument lists with a trailing comma which are
   /// considered "collection-like". In that case, [node] is `null`.
-  void _visitCollectionLiteral(TypedLiteral node, Token leftBracket,
+  void _visitCollectionLiteral(TypedLiteral? node, Token leftBracket,
       Iterable<AstNode> elements, Token rightBracket,
-      [int cost]) {
+      [int? cost]) {
     if (node != null) {
       // See if `const` should be removed.
       if (node.constKeyword != null &&
           _constNesting > 0 &&
           _formatter.fixes.contains(StyleFix.optionalConst)) {
         // Don't lose comments before the discarded keyword, if any.
-        writePrecedingCommentsAndNewlines(node.constKeyword);
+        writePrecedingCommentsAndNewlines(node.constKeyword!);
       } else {
         modifier(node.constKeyword);
       }
@@ -3165,7 +3157,7 @@
       if (element != elements.first) {
         if (preserveNewlines) {
           // See if the next element is on the next line.
-          if (_endLine(element.beginToken.previous) !=
+          if (_endLine(element.beginToken.previous!) !=
               _startLine(element.beginToken)) {
             oneOrTwoNewlines();
 
@@ -3219,7 +3211,7 @@
 
     // Find the parameter immediately preceding the optional parameters (if
     // there are any).
-    FormalParameter lastRequired;
+    FormalParameter? lastRequired;
     for (var i = 0; i < parameters.parameters.length; i++) {
       if (parameters.parameters[i] is DefaultFormalParameter) {
         if (i > 0) lastRequired = parameters.parameters[i - 1];
@@ -3286,10 +3278,10 @@
   /// In that case [functionKeywordPosition] should be the source position
   /// used for the inserted "Function" text.
   void _visitGenericFunctionType(
-      AstNode returnType,
-      Token functionKeyword,
-      int functionKeywordPosition,
-      TypeParameterList typeParameters,
+      AstNode? returnType,
+      Token? functionKeyword,
+      int? functionKeywordPosition,
+      TypeParameterList? typeParameters,
       FormalParameterList parameters) {
     builder.startLazyRule();
     builder.nestExpression();
@@ -3298,7 +3290,7 @@
     if (functionKeyword != null) {
       token(functionKeyword);
     } else {
-      _writeText('Function', functionKeywordPosition);
+      _writeText('Function', functionKeywordPosition!);
     }
 
     builder.unnest();
@@ -3312,7 +3304,7 @@
   /// If [equals] is `null`, then [equalsPosition] must be a
   /// position to use for the inserted text "=".
   void _visitGenericTypeAliasHeader(Token typedefKeyword, AstNode name,
-      AstNode typeParameters, Token equals, int equalsPosition) {
+      AstNode? typeParameters, Token? equals, int? equalsPosition) {
     token(typedefKeyword);
     space();
 
@@ -3329,7 +3321,7 @@
     if (equals != null) {
       token(equals);
     } else {
-      _writeText('=', equalsPosition);
+      _writeText('=', equalsPosition!);
     }
 
     builder.endRule();
@@ -3362,7 +3354,7 @@
   ///     //   ^
   ///
   /// Otherwise, returns `null`.
-  Token _findSpreadCollectionBracket(AstNode node) {
+  Token? _findSpreadCollectionBracket(AstNode? node) {
     if (node is SpreadElement) {
       var expression = node.expression;
       if (expression is ListLiteral) {
@@ -3445,15 +3437,8 @@
     // See if this literal is associated with an argument list or if element
     // that wants to handle splitting and indenting it. If not, we'll use a
     // default rule.
-    Rule rule;
-    if (_blockRules.containsKey(leftBracket)) {
-      rule = _blockRules[leftBracket];
-    }
-
-    Chunk argumentChunk;
-    if (_blockPreviousChunks.containsKey(leftBracket)) {
-      argumentChunk = _blockPreviousChunks[leftBracket];
-    }
+    var rule = _blockRules[leftBracket];
+    var argumentChunk = _blockPreviousChunks[leftBracket];
 
     // Create a rule for whether or not to split the block contents.
     builder.startRule(rule);
@@ -3468,7 +3453,7 @@
   /// given, ignores that rule inside the body when determining if it should
   /// split.
   void _endLiteralBody(Token rightBracket,
-      {Rule ignoredRule, bool forceSplit}) {
+      {Rule? ignoredRule, bool? forceSplit}) {
     forceSplit ??= false;
 
     // Put comments before the closing delimiter inside the block.
@@ -3508,26 +3493,26 @@
   void _visitCombinator(Token keyword, Iterable<AstNode> nodes) {
     // Allow splitting before the keyword.
     var rule = builder.rule as CombinatorRule;
-    rule.addCombinator(split());
+    rule.addCombinator(split()!);
 
     builder.nestExpression();
     token(keyword);
 
-    rule.addName(split());
-    visitCommaSeparatedNodes(nodes, between: () => rule.addName(split()));
+    rule.addName(split()!);
+    visitCommaSeparatedNodes(nodes, between: () => rule.addName(split()!));
 
     builder.unnest();
   }
 
   /// If [keyword] is `const`, begins a new constant context.
-  void _startPossibleConstContext(Token keyword) {
+  void _startPossibleConstContext(Token? keyword) {
     if (keyword != null && keyword.keyword == Keyword.CONST) {
       _constNesting++;
     }
   }
 
   /// If [keyword] is `const`, ends the current outermost constant context.
-  void _endPossibleConstContext(Token keyword) {
+  void _endPossibleConstContext(Token? keyword) {
     if (keyword != null && keyword.keyword == Keyword.CONST) {
       _constNesting--;
     }
@@ -3554,7 +3539,7 @@
   /// splitting rule for the block. These are used for handling block-like
   /// expressions inside argument lists and spread collections inside if
   /// elements.
-  void beforeBlock(Token token, Rule rule, [Chunk previousChunk]) {
+  void beforeBlock(Token token, Rule rule, [Chunk? previousChunk]) {
     _blockRules[token] = rule;
     if (previousChunk != null) _blockPreviousChunks[token] = previousChunk;
   }
@@ -3587,7 +3572,7 @@
   /// [FunctionExpression].
   bool _isInLambda(AstNode node) =>
       node.parent is FunctionExpression &&
-      node.parent.parent is! FunctionDeclaration;
+      node.parent!.parent is! FunctionDeclaration;
 
   /// Writes the string literal [string] to the output.
   ///
@@ -3599,7 +3584,7 @@
     writePrecedingCommentsAndNewlines(string);
 
     // Split each line of a multiline string into separate chunks.
-    var lines = string.lexeme.split(_formatter.lineEnding);
+    var lines = string.lexeme.split(_formatter.lineEnding!);
     var offset = string.offset;
 
     _writeText(lines.first, offset);
@@ -3622,16 +3607,14 @@
   bool hasCommaAfter(AstNode node) => _commaAfter(node) != null;
 
   /// The comma token immediately following [node] if there is one, or `null`.
-  Token _commaAfter(AstNode node) {
-    if (node.endToken.next.type == TokenType.COMMA) {
-      return node.endToken.next;
-    }
+  Token? _commaAfter(AstNode node) {
+    var next = node.endToken.next!;
+    if (next.type == TokenType.COMMA) return next;
 
     // TODO(sdk#38990): endToken doesn't include the "?" on a nullable
     // function-typed formal, so check for that case and handle it.
-    if (node.endToken.next.type == TokenType.QUESTION &&
-        node.endToken.next.next.type == TokenType.COMMA) {
-      return node.endToken.next.next;
+    if (next.type == TokenType.QUESTION && next.next!.type == TokenType.COMMA) {
+      return next.next;
     }
 
     return null;
@@ -3639,7 +3622,7 @@
 
   /// Emit the given [modifier] if it's non null, followed by non-breaking
   /// whitespace.
-  void modifier(Token modifier) {
+  void modifier(Token? modifier) {
     token(modifier, after: space);
   }
 
@@ -3682,15 +3665,15 @@
   /// Writes a single space split owned by the current rule.
   ///
   /// Returns the chunk the split was applied to.
-  Chunk split() => builder.split(space: true);
+  Chunk? split() => builder.split(space: true);
 
   /// Writes a zero-space split owned by the current rule.
   ///
   /// Returns the chunk the split was applied to.
-  Chunk zeroSplit() => builder.split();
+  Chunk? zeroSplit() => builder.split();
 
   /// Writes a single space split with its own rule.
-  Rule soloSplit([int cost]) {
+  Rule soloSplit([int? cost]) {
     var rule = Rule(cost);
     builder.startRule(rule);
     split();
@@ -3711,7 +3694,7 @@
   /// Does nothing if [token] is `null`. If [before] is given, it will be
   /// executed before the token is outout. Likewise, [after] will be called
   /// after the token is output.
-  void token(Token token, {void Function() before, void Function() after}) {
+  void token(Token? token, {void Function()? before, void Function()? after}) {
     if (token == null) return;
 
     writePrecedingCommentsAndNewlines(token);
@@ -3725,13 +3708,13 @@
 
   /// Writes all formatted whitespace and comments that appear before [token].
   bool writePrecedingCommentsAndNewlines(Token token) {
-    var comment = token.precedingComments;
+    Token? comment = token.precedingComments;
 
     // For performance, avoid calculating newlines between tokens unless
     // actually needed.
     if (comment == null) {
       if (builder.needsToPreserveNewlines) {
-        builder.preserveNewlines(_startLine(token) - _endLine(token.previous));
+        builder.preserveNewlines(_startLine(token) - _endLine(token.previous!));
       }
 
       return false;
@@ -3740,14 +3723,14 @@
     // If the token's comments are being moved by a fix, do not write them here.
     if (_suppressPrecedingCommentsAndNewLines.contains(token)) return false;
 
-    var previousLine = _endLine(token.previous);
+    var previousLine = _endLine(token.previous!);
     var tokenLine = _startLine(token);
 
     // Edge case: The analyzer includes the "\n" in the script tag's lexeme,
     // which confuses some of these calculations. We don't want to allow a
     // blank line between the script tag and a following comment anyway, so
     // just override the script tag's line.
-    if (token.previous.type == TokenType.SCRIPT_TAG) previousLine = tokenLine;
+    if (token.previous!.type == TokenType.SCRIPT_TAG) previousLine = tokenLine;
 
     var comments = <SourceComment>[];
     while (comment != null) {
@@ -3755,7 +3738,7 @@
 
       // Don't preserve newlines at the top of the file.
       if (comment == token.precedingComments &&
-          token.previous.type == TokenType.EOF) {
+          token.previous!.type == TokenType.EOF) {
         previousLine = commentLine;
       }
 
@@ -3824,14 +3807,14 @@
   ///
   /// Returns `null` if the selection start has already been processed or is
   /// not within that range.
-  int _getSelectionStartWithin(int offset, int length) {
+  int? _getSelectionStartWithin(int offset, int length) {
     // If there is no selection, do nothing.
     if (_source.selectionStart == null) return null;
 
     // If we've already passed it, don't consider it again.
     if (_passedSelectionStart) return null;
 
-    var start = _source.selectionStart - offset;
+    var start = _source.selectionStart! - offset;
 
     // If it started in whitespace before this text, push it forward to the
     // beginning of the non-whitespace text.
@@ -3851,7 +3834,7 @@
   ///
   /// Returns `null` if the selection endpoint has already been processed or is
   /// not within that range.
-  int _getSelectionEndWithin(int offset, int length) {
+  int? _getSelectionEndWithin(int offset, int length) {
     // If there is no selection, do nothing.
     if (_source.selectionLength == null) return null;
 
@@ -3882,28 +3865,32 @@
   ///
   /// Removes any trailing whitespace from the selection.
   int _findSelectionEnd() {
-    if (_selectionEnd != null) return _selectionEnd;
+    if (_selectionEnd != null) return _selectionEnd!;
 
-    _selectionEnd = _source.selectionStart + _source.selectionLength;
+    var end = _source.selectionStart! + _source.selectionLength!;
 
     // If the selection bumps to the end of the source, pin it there.
-    if (_selectionEnd == _source.text.length) return _selectionEnd;
+    if (end == _source.text.length) {
+      _selectionEnd = end;
+      return end;
+    }
 
     // Trim off any trailing whitespace. We want the selection to "rubberband"
     // around the selected non-whitespace tokens since the whitespace will
     // be munged by the formatter itself.
-    while (_selectionEnd > _source.selectionStart) {
+    while (end > _source.selectionStart!) {
       // Stop if we hit anything other than space, tab, newline or carriage
       // return.
-      var char = _source.text.codeUnitAt(_selectionEnd - 1);
+      var char = _source.text.codeUnitAt(end - 1);
       if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
         break;
       }
 
-      _selectionEnd--;
+      end--;
     }
 
-    return _selectionEnd;
+    _selectionEnd = end;
+    return end;
   }
 
   /// Gets the 1-based line number that the beginning of [token] lies on.
diff --git a/lib/src/whitespace.dart b/lib/src/whitespace.dart
index bf7eab2..d9f226d 100644
--- a/lib/src/whitespace.dart
+++ b/lib/src/whitespace.dart
@@ -71,6 +71,10 @@
   /// less prescriptive over the user's whitespace.
   static const oneOrTwoNewlines = Whitespace._('oneOrTwoNewlines');
 
+  /// A hard split was just written whose whitespace takes precedence over any
+  /// previous pending whitespace.
+  static const afterHardSplit = Whitespace._('afterHardSplit');
+
   final String name;
 
   /// Gets the minimum number of newlines contained in this whitespace.
diff --git a/pubspec.lock b/pubspec.lock
index 2981e21..bed7945 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -84,7 +84,7 @@
       name: file
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "6.0.0"
+    version: "6.1.0"
   glob:
     dependency: transitive
     description:
@@ -224,7 +224,7 @@
       name: shelf_web_socket
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.4"
+    version: "0.2.4+1"
   source_map_stack_trace:
     dependency: transitive
     description:
@@ -280,7 +280,7 @@
       name: test
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.16.2"
+    version: "1.16.3"
   test_api:
     dependency: transitive
     description:
@@ -301,14 +301,14 @@
       name: test_descriptor
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.1"
+    version: "2.0.0"
   test_process:
     dependency: "direct dev"
     description:
       name: test_process
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.5"
+    version: "2.0.0"
   typed_data:
     dependency: transitive
     description:
@@ -322,7 +322,7 @@
       name: vm_service
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "6.0.1"
+    version: "6.1.0+1"
   watcher:
     dependency: transitive
     description:
@@ -336,7 +336,7 @@
       name: web_socket_channel
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "2.0.0"
   webkit_inspection_protocol:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index e387681..4ad9d21 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,13 +1,13 @@
 name: dart_style
 # Note: See tool/grind.dart for how to bump the version.
-version: 1.3.14
+version: 2.0.0-dev
 description: >-
   Opinionated, automatic Dart source code formatter.
   Provides an API and a CLI tool.
 repository: https://github.com/dart-lang/dart_style
 
 environment:
-  sdk: '>=2.11.99 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
   analyzer: ^1.0.0
@@ -22,8 +22,8 @@
   node_preamble: ^1.0.0
   pedantic: ^1.0.0
   test: ^1.16.0
-  test_descriptor: ^1.0.0
-  test_process: ^1.0.0
+  test_descriptor: ^2.0.0
+  test_process: ^2.0.0
   yaml: '>=2.0.0 <4.0.0'
 
 executables:
diff --git a/test/utils.dart b/test/utils.dart
index b22f73f..926841f 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -27,10 +27,10 @@
 
 /// If tool/command_shell.dart has been compiled to a snapshot, this is the path
 /// to it.
-String _commandExecutablePath;
+String? _commandExecutablePath;
 
 /// If bin/format.dart has been compiled to a snapshot, this is the path to it.
-String _formatterExecutablePath;
+String? _formatterExecutablePath;
 
 /// Compiles format.dart to a native executable for tests to use.
 ///
@@ -42,7 +42,7 @@
   });
 
   tearDownAll(() async {
-    await _deleteSnapshot(_formatterExecutablePath);
+    await _deleteSnapshot(_formatterExecutablePath!);
     _formatterExecutablePath = null;
   });
 }
@@ -57,7 +57,7 @@
   });
 
   tearDownAll(() async {
-    await _deleteSnapshot(_commandExecutablePath);
+    await _deleteSnapshot(_commandExecutablePath!);
     _commandExecutablePath = null;
   });
 }
@@ -108,41 +108,41 @@
 }
 
 /// Runs the command line formatter, passing it [args].
-Future<TestProcess> runFormatter([List<String> args]) {
+Future<TestProcess> runFormatter([List<String>? args]) {
   if (_formatterExecutablePath == null) {
     fail('Must call createFormatterExecutable() before running commands.');
   }
 
   return TestProcess.start(
-      Platform.resolvedExecutable, [_formatterExecutablePath, ...?args],
+      Platform.resolvedExecutable, [_formatterExecutablePath!, ...?args],
       workingDirectory: d.sandbox);
 }
 
 /// Runs the command line formatter, passing it the test directory followed by
 /// [args].
-Future<TestProcess> runFormatterOnDir([List<String> args]) {
+Future<TestProcess> runFormatterOnDir([List<String>? args]) {
   return runFormatter(['.', ...?args]);
 }
 
 /// Runs the test shell for the [Command]-based formatter, passing it [args].
-Future<TestProcess> runCommand([List<String> args]) {
+Future<TestProcess> runCommand([List<String>? args]) {
   if (_commandExecutablePath == null) {
     fail('Must call createCommandExecutable() before running commands.');
   }
 
-  return TestProcess.start(
-      Platform.resolvedExecutable, [_commandExecutablePath, 'format', ...?args],
+  return TestProcess.start(Platform.resolvedExecutable,
+      [_commandExecutablePath!, 'format', ...?args],
       workingDirectory: d.sandbox);
 }
 
 /// Runs the test shell for the [Command]-based formatter, passing it the test
 /// directory followed by [args].
-Future<TestProcess> runCommandOnDir([List<String> args]) {
+Future<TestProcess> runCommandOnDir([List<String>? args]) {
   return runCommand(['.', ...?args]);
 }
 
 /// Run tests defined in "*.unit" and "*.stmt" files inside directory [name].
-void testDirectory(String name, [Iterable<StyleFix> fixes]) {
+void testDirectory(String name, [Iterable<StyleFix>? fixes]) {
   // Locate the "test" directory. Use mirrors so that this works with the test
   // package, which loads this suite into an isolate.
   // TODO(rnystrom): Investigate using Isolate.resolvePackageUri instead.
@@ -164,7 +164,7 @@
   }
 }
 
-void testFile(String path, [Iterable<StyleFix> fixes]) {
+void testFile(String path, [Iterable<StyleFix>? fixes]) {
   // Locate the "test" directory. Use mirrors so that this works with the test
   // package, which loads this suite into an isolate.
   var testDir = p.dirname(currentMirrorSystem()
@@ -175,9 +175,8 @@
   _testFile(p.dirname(path), p.join(testDir, path), fixes);
 }
 
-void _testFile(String name, String path, Iterable<StyleFix> baseFixes) {
-  var fixes = <StyleFix>[];
-  if (baseFixes != null) fixes.addAll(baseFixes);
+void _testFile(String name, String path, Iterable<StyleFix>? baseFixes) {
+  var fixes = [...?baseFixes];
 
   group('$name ${p.basename(path)}', () {
     // Explicitly create a File, in case the entry is a Link.
@@ -198,7 +197,7 @@
       // regression tests which often come from a chunk of nested code.
       var leadingIndent = 0;
       description = description.replaceAllMapped(_indentPattern, (match) {
-        leadingIndent = int.parse(match[1]);
+        leadingIndent = int.parse(match[1]!);
         return '';
       });
 
@@ -294,7 +293,7 @@
 /// accidentally modify the Dart code being formatted.
 String _unescapeUnicode(String input) {
   return input.replaceAllMapped(_unicodePattern, (match) {
-    var codePoint = int.parse(match[1], radix: 16);
+    var codePoint = int.parse(match[1]!, radix: 16);
     return String.fromCharCode(codePoint);
   });
 }