Merge branch 'master' into bump
diff --git a/lib/src/call_chain_visitor.dart b/lib/src/call_chain_visitor.dart
index 5af43f7..4151cb7 100644
--- a/lib/src/call_chain_visitor.dart
+++ b/lib/src/call_chain_visitor.dart
@@ -558,7 +558,7 @@
   }
 
   // Postfix expressions.
-  if (node is IndexExpression) {
+  if (node is IndexExpression && node.target != null) {
     return _unwrapPostfix(node, node.target!, calls);
   }
 
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 02dbfb6..90d5636 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -87,8 +87,8 @@
   ///     someFunctionName(argument, argument,
   ///         argument, anotherFunction(argument,
   ///             argument));
-  NestingLevel get nesting => _nesting;
-  late NestingLevel _nesting;
+  NestingLevel? get nesting => _nesting;
+  NestingLevel? _nesting;
 
   /// If this chunk marks the beginning of a block, this contains the child
   /// chunks and other data about that nested block.
@@ -178,6 +178,12 @@
   /// Creates a new chunk starting with [_text].
   Chunk(this._text);
 
+  /// Creates a dummy chunk.
+  ///
+  /// This is returned in some places by [ChunkBuilder] when there is no useful
+  /// chunk to yield and it will not end up being used by the caller anyway.
+  Chunk.dummy() : _text = '(dummy)';
+
   /// Discard the split for the chunk and put it back into the state where more
   /// text can be appended.
   void allowText() {
@@ -232,7 +238,14 @@
     var argument = block.argument;
     if (argument == null) return false;
 
-    return argument.rule!.isSplit(getValue(argument.rule!), argument);
+    var rule = argument.rule;
+
+    // There may be no rule if the block occurs inside a string interpolation.
+    // In that case, it's not clear if anything will look particularly nice, but
+    // expression nesting is probably marginally better.
+    if (rule == null) return true;
+
+    return rule.isSplit(getValue(rule), argument);
   }
 
   // Mark whether this chunk can divide the range of chunks.
diff --git a/lib/src/chunk_builder.dart b/lib/src/chunk_builder.dart
index a17c89a..c2ab67b 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -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
@@ -189,7 +189,7 @@
     // discarded because no chunk references it.
     if (_preventSplitNesting > 0) {
       if (space) _pendingWhitespace = Whitespace.space;
-      return null;
+      return Chunk.dummy();
     }
 
     return _writeSplit(_rules.last,
@@ -847,15 +847,14 @@
   /// Ends the current chunk (if any) with the given split information.
   ///
   /// Returns the chunk.
-  Chunk? _writeSplit(Rule rule,
+  Chunk _writeSplit(Rule rule,
       {bool? flushLeft, bool? isDouble, bool? nest, bool? space}) {
     nest ??= true;
     space ??= false;
 
     if (_chunks.isEmpty) {
       if (flushLeft != null) _firstFlushLeft = flushLeft;
-
-      return null;
+      return Chunk.dummy();
     }
 
     _chunks.last.applySplit(
@@ -884,7 +883,7 @@
 
     var chunk = _chunks[i];
     if (!chunk.rule!.isHardened) return false;
-    if (chunk.nesting.isNested) return false;
+    if (chunk.nesting!.isNested) return false;
     if (chunk.isBlock) return false;
 
     return true;
diff --git a/lib/src/debug.dart b/lib/src/debug.dart
index 5934afd..f796af3 100644
--- a/lib/src/debug.dart
+++ b/lib/src/debug.dart
@@ -146,7 +146,7 @@
     writeIf(chunk.indent != null && chunk.indent != 0,
         () => 'indent ${chunk.indent}');
 
-    writeIf(chunk.nesting.indent != 0, () => 'nest ${chunk.nesting}');
+    writeIf(chunk.nesting?.indent != 0, () => 'nest ${chunk.nesting}');
 
     writeIf(chunk.flushLeft, () => 'flush');
 
diff --git a/lib/src/line_splitting/solve_state.dart b/lib/src/line_splitting/solve_state.dart
index 708353e..6701c7a 100644
--- a/lib/src/line_splitting/solve_state.dart
+++ b/lib/src/line_splitting/solve_state.dart
@@ -279,8 +279,8 @@
     for (var i = 0; i < _splitter.chunks.length - 1; i++) {
       var chunk = _splitter.chunks[i];
       if (chunk.rule!.isSplit(getValue(chunk.rule!), chunk)) {
-        usedNestingLevels.add(chunk.nesting);
-        chunk.nesting.clearTotalUsedIndent();
+        usedNestingLevels.add(chunk.nesting!);
+        chunk.nesting!.clearTotalUsedIndent();
       }
     }
 
@@ -298,7 +298,7 @@
           indent = _splitter.blockIndentation + chunk.indent!;
 
           // And any expression nesting.
-          indent += chunk.nesting.totalUsedIndent;
+          indent += chunk.nesting!.totalUsedIndent;
 
           if (chunk.indentBlock(getValue)) indent += Indent.expression;
         }
@@ -385,9 +385,10 @@
         // But there are a couple of squirrely cases where it's hard to prevent
         // by construction. Instead, this outlaws it by penalizing it very
         // heavily if it happens to get this far.
+        var totalIndent = chunk.nesting!.totalUsedIndent;
         if (previousNesting != null &&
-            chunk.nesting.totalUsedIndent != 0 &&
-            chunk.nesting.totalUsedIndent == previousNesting.totalUsedIndent &&
+            totalIndent != 0 &&
+            totalIndent == previousNesting.totalUsedIndent &&
             !identical(chunk.nesting, previousNesting)) {
           _overflowChars += 10000;
         }
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index 50986a4..6a2ccd4 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -2879,7 +2879,7 @@
     builder.nestExpression();
 
     token(leftBracket);
-    rule.beforeArgument(zeroSplit()!);
+    rule.beforeArgument(zeroSplit());
 
     for (var node in nodes) {
       visit(node);
@@ -2887,7 +2887,7 @@
       // Write the trailing comma.
       if (node != nodes.last) {
         token(node.endToken.next);
-        rule.beforeArgument(split()!);
+        rule.beforeArgument(split());
       }
     }
 
@@ -3493,13 +3493,13 @@
   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();
   }
@@ -3665,12 +3665,12 @@
   /// 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]) {
diff --git a/test/regression/other/null_safety.unit b/test/regression/other/null_safety.unit
new file mode 100644
index 0000000..5dc376d
--- /dev/null
+++ b/test/regression/other/null_safety.unit
@@ -0,0 +1,20 @@
+>>> issues found when migrating to null safety
+void main() => print('first: ${first<int>([1, 2, 3])}');
+<<<
+void main() => print('first: ${first<int>([1, 2, 3])}');
+>>>
+var x = '${jsonEncode([
+  1,
+])}';
+<<<
+var x = '${jsonEncode([
+      1,
+    ])}';
+>>>
+main() {
+  patch..[entityId].removeWhere();
+}
+<<<
+main() {
+  patch..[entityId].removeWhere();
+}
\ No newline at end of file
diff --git a/test/splitting/invocations.stmt b/test/splitting/invocations.stmt
index 17948be..6650f93 100644
--- a/test/splitting/invocations.stmt
+++ b/test/splitting/invocations.stmt
@@ -159,6 +159,20 @@
     .method()
       ..x = 1
       ..y = 2;
+>>> cascade index
+object..[index]..method()..[index]=value;
+<<<
+object
+  ..[index]
+  ..method()
+  ..[index] = value;
+>>> null-aware cascade index
+object?..[index]..method()..[index]=value;
+<<<
+object
+  ?..[index]
+  ..method()
+  ..[index] = value;
 >>> conditional invocation
 object?.method().method()?.method().method();
 <<<