Add StringInterpolation.firstString and lastString.

There are several places in linter where we cast unconditionally.

Potentially we could also have `firstExpression` because there is
always at least one.

Change-Id: I9c3e754de6c8872941c1f1a47e36a6cbd3714b4a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/201362
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/CHANGELOG.md b/pkg/analyzer/CHANGELOG.md
index 0fb2388..2dd64a6 100644
--- a/pkg/analyzer/CHANGELOG.md
+++ b/pkg/analyzer/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.8.0-dev
+* Added `StringInterpolation.firstString` and `lastString`, to express
+  explicitly  that there are always (possibly empty) strings as the first
+  and the last elements of an interpolation.
+
 ## 1.7.0
 * Require `meta: ^1.4.0`.
 
diff --git a/pkg/analyzer/lib/dart/ast/ast.dart b/pkg/analyzer/lib/dart/ast/ast.dart
index aafc82f..e415663 100644
--- a/pkg/analyzer/lib/dart/ast/ast.dart
+++ b/pkg/analyzer/lib/dart/ast/ast.dart
@@ -5152,7 +5152,18 @@
 /// Clients may not extend, implement or mix-in this class.
 abstract class StringInterpolation implements SingleStringLiteral {
   /// Return the elements that will be composed to produce the resulting string.
+  /// The list includes [firstString] and [lastString].
   NodeList<InterpolationElement> get elements;
+
+  /// Return the first element in this interpolation, which is always a string.
+  /// The string might be empty if there is no text before the first
+  /// interpolation expression (such as in `'$foo bar'`).
+  InterpolationString get firstString;
+
+  /// Return the last element in this interpolation, which is always a string.
+  /// The string might be empty if there is no text after the last
+  /// interpolation expression (such as in `'foo $bar'`).
+  InterpolationString get lastString;
 }
 
 /// A string literal expression.
diff --git a/pkg/analyzer/lib/src/dart/ast/ast.dart b/pkg/analyzer/lib/src/dart/ast/ast.dart
index e02e41b..ea7e858 100644
--- a/pkg/analyzer/lib/src/dart/ast/ast.dart
+++ b/pkg/analyzer/lib/src/dart/ast/ast.dart
@@ -9167,6 +9167,20 @@
 
   /// Initialize a newly created string interpolation expression.
   StringInterpolationImpl(List<InterpolationElement> elements) {
+    // TODO(scheglov) Replace asserts with appropriately typed parameters.
+    assert(elements.length > 2, 'Expected at last three elements.');
+    assert(
+      elements.first is InterpolationStringImpl,
+      'The first element must be a string.',
+    );
+    assert(
+      elements[1] is InterpolationExpressionImpl,
+      'The second element must be an expression.',
+    );
+    assert(
+      elements.last is InterpolationStringImpl,
+      'The last element must be a string.',
+    );
     _elements._initialize(this, elements);
   }
 
@@ -9197,6 +9211,10 @@
   Token get endToken => _elements.endToken!;
 
   @override
+  InterpolationStringImpl get firstString =>
+      elements.first as InterpolationStringImpl;
+
+  @override
   bool get isMultiline => _firstHelper.isMultiline;
 
   @override
@@ -9205,6 +9223,10 @@
   @override
   bool get isSingleQuoted => _firstHelper.isSingleQuoted;
 
+  @override
+  InterpolationStringImpl get lastString =>
+      elements.last as InterpolationStringImpl;
+
   StringLexemeHelper get _firstHelper {
     var lastString = _elements.first as InterpolationString;
     String lexeme = lastString.contents.lexeme;
diff --git a/pkg/analyzer/pubspec.yaml b/pkg/analyzer/pubspec.yaml
index e2c3a5c..b3cbff8 100644
--- a/pkg/analyzer/pubspec.yaml
+++ b/pkg/analyzer/pubspec.yaml
@@ -1,5 +1,5 @@
 name: analyzer
-version: 1.7.0
+version: 1.8.0-dev
 description: This package provides a library that performs static analysis of Dart code.
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/analyzer
 
diff --git a/pkg/analyzer/test/dart/ast/ast_test.dart b/pkg/analyzer/test/dart/ast/ast_test.dart
index e3fd7f5..5fcfbc6 100644
--- a/pkg/analyzer/test/dart/ast/ast_test.dart
+++ b/pkg/analyzer/test/dart/ast/ast_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/analysis/features.dart';
+import 'package:analyzer/dart/analysis/utilities.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/src/dart/ast/ast_factory.dart';
@@ -430,171 +431,155 @@
 
 @reflectiveTest
 class InterpolationStringTest extends ParserTestCase {
-  InterpolationString interpolationString(
-      String lexeme, String value, bool isFirst, bool isLast) {
-    var node = AstTestFactory.interpolationString(lexeme, value);
-    var nodes = <InterpolationElement>[
-      if (!isFirst) AstTestFactory.interpolationString("'first", "first"),
-      node,
-      if (!isLast) AstTestFactory.interpolationString("last'", "last")
-    ];
-    var parent = AstTestFactory.string(nodes);
-    assert(node.parent == parent);
-    return node;
-  }
+  /// This field is updated in [_parseStringInterpolation].
+  /// It is used in [_assertContentsOffsetEnd].
+  var _baseOffset = 0;
 
   void test_contentsOffset_doubleQuote_first() {
-    var node = interpolationString('"foo', "foo", true, true);
-    expect(node.contentsOffset, '"'.length);
-    expect(node.contentsEnd, '"'.length + "foo".length);
-  }
-
-  void test_contentsOffset_doubleQuote_firstLast() {
-    var node = interpolationString('"foo"', "foo", true, true);
-    expect(node.contentsOffset, '"'.length);
-    expect(node.contentsEnd, '"'.length + "foo".length);
+    var interpolation = _parseStringInterpolation('"foo\$x last"');
+    var node = interpolation.firstString;
+    _assertContentsOffsetEnd(node, 1, 4);
   }
 
   void test_contentsOffset_doubleQuote_last() {
-    var node = interpolationString('foo"', "foo", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo".length);
+    var interpolation = _parseStringInterpolation('"first \$x foo"');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 13);
   }
 
   void test_contentsOffset_doubleQuote_last_empty() {
-    var node = interpolationString('"', "", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, 0);
+    var interpolation = _parseStringInterpolation('"first \$x"');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 9);
   }
 
   void test_contentsOffset_doubleQuote_last_unterminated() {
-    var node = interpolationString('foo', "foo", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo".length);
+    var interpolation = _parseStringInterpolation('"first \$x foo');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 13);
   }
 
   void test_contentsOffset_doubleQuote_multiline_first() {
-    var node = interpolationString('"""\nfoo\n', "foo\n", true, true);
-    expect(node.contentsOffset, '"""\n'.length);
-    expect(node.contentsEnd, '"""\n'.length + "foo\n".length);
-  }
-
-  void test_contentsOffset_doubleQuote_multiline_firstLast() {
-    var node = interpolationString('"""\nfoo\n"""', "foo\n", true, true);
-    expect(node.contentsOffset, '"""\n'.length);
-    expect(node.contentsEnd, '"""\n'.length + "foo\n".length);
+    var interpolation = _parseStringInterpolation('"""foo\n\$x last"""');
+    var node = interpolation.firstString;
+    _assertContentsOffsetEnd(node, 3, 7);
   }
 
   void test_contentsOffset_doubleQuote_multiline_last() {
-    var node = interpolationString('foo\n"""', "foo\n", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo\n".length);
+    var interpolation = _parseStringInterpolation('"""first\$x foo\n"""');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 15);
   }
 
   void test_contentsOffset_doubleQuote_multiline_last_empty() {
-    var node = interpolationString('"""', "", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, 0);
+    var interpolation = _parseStringInterpolation('"""first\$x"""');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 10);
   }
 
   void test_contentsOffset_doubleQuote_multiline_last_unterminated() {
-    var node = interpolationString('foo\n', "foo\n", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo\n".length);
+    var interpolation = _parseStringInterpolation('"""first\$x foo\n');
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 15);
   }
 
   void test_contentsOffset_escapeCharacters() {
     // Contents offset cannot use 'value' string, because of escape sequences.
-    var node = interpolationString(r'"foo\nbar"', "foo\nbar", true, true);
-    expect(node.contentsOffset, '"'.length);
-    expect(node.contentsEnd, '"'.length + "foo\\nbar".length);
+    var interpolation = _parseStringInterpolation(r'"foo\nbar$x last"');
+    var node = interpolation.firstString;
+    _assertContentsOffsetEnd(node, 1, 9);
   }
 
   void test_contentsOffset_middle() {
-    var node = interpolationString("foo", "foo", false, false);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo".length);
+    var interpolation =
+        _parseStringInterpolation(r'"first $x foo\nbar $y last"');
+    var node = interpolation.elements[2] as InterpolationString;
+    _assertContentsOffsetEnd(node, 9, 19);
   }
 
   void test_contentsOffset_middle_quoteBegin() {
-    // This occurs in, for instance, `"$a'foo$b"`
-    var node = interpolationString("'foo", "'foo", false, false);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "'foo".length);
+    var interpolation = _parseStringInterpolation('"first \$x \'foo\$y last"');
+    var node = interpolation.elements[2] as InterpolationString;
+    _assertContentsOffsetEnd(node, 9, 14);
   }
 
   void test_contentsOffset_middle_quoteBeginEnd() {
-    // This occurs in, for instance, `"$a'foo'$b"`
-    var node = interpolationString("'foo'", "'foo'", false, false);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "'foo'".length);
+    var interpolation =
+        _parseStringInterpolation('"first \$x \'foo\'\$y last"');
+    var node = interpolation.elements[2] as InterpolationString;
+    _assertContentsOffsetEnd(node, 9, 15);
   }
 
   void test_contentsOffset_middle_quoteEnd() {
-    // This occurs in, for instance, `"${a}foo'$b"`
-    var node = interpolationString("foo'", "foo'", false, false);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo'".length);
+    var interpolation = _parseStringInterpolation('"first \$x foo\'\$y last"');
+    var node = interpolation.elements[2] as InterpolationString;
+    _assertContentsOffsetEnd(node, 9, 14);
   }
 
   void test_contentsOffset_singleQuote_first() {
-    var node = interpolationString("'foo", "foo", true, true);
-    expect(node.contentsOffset, "'".length);
-    expect(node.contentsEnd, "'".length + "foo".length);
-  }
-
-  void test_contentsOffset_singleQuote_firstLast() {
-    var node = interpolationString("'foo'", "foo", true, true);
-    expect(node.contentsOffset, "'".length);
-    expect(node.contentsEnd, "'".length + "foo".length);
+    var interpolation = _parseStringInterpolation("'foo\$x last'");
+    var node = interpolation.firstString;
+    _assertContentsOffsetEnd(node, 1, 4);
   }
 
   void test_contentsOffset_singleQuote_last() {
-    var node = interpolationString("foo'", "foo", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo".length);
+    var interpolation = _parseStringInterpolation("'first \$x foo'");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 13);
   }
 
   void test_contentsOffset_singleQuote_last_empty() {
-    var node = interpolationString("'", "", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, 0);
+    var interpolation = _parseStringInterpolation("'first \$x'");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 9);
   }
 
   void test_contentsOffset_singleQuote_last_unterminated() {
-    var node = interpolationString("foo", "foo", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo".length);
+    var interpolation = _parseStringInterpolation("'first \$x");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 9, 9);
   }
 
   void test_contentsOffset_singleQuote_multiline_first() {
-    var node = interpolationString("'''\nfoo\n", "foo\n", true, true);
-    expect(node.contentsOffset, "'''\n".length);
-    expect(node.contentsEnd, "'''\n".length + "foo\n".length);
-  }
-
-  void test_contentsOffset_singleQuote_multiline_firstLast() {
-    var node = interpolationString("'''\nfoo\n'''", "foo\n", true, true);
-    expect(node.contentsOffset, "'''\n".length);
-    expect(node.contentsEnd, "'''\n".length + "foo\n".length);
+    var interpolation = _parseStringInterpolation("'''foo\n\$x last'''");
+    var node = interpolation.firstString;
+    _assertContentsOffsetEnd(node, 3, 7);
   }
 
   void test_contentsOffset_singleQuote_multiline_last() {
-    var node = interpolationString("foo\n'''", "foo\n", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo\n".length);
+    var interpolation = _parseStringInterpolation("'''first\$x foo\n'''");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 15);
   }
 
   void test_contentsOffset_singleQuote_multiline_last_empty() {
-    var node = interpolationString("'''", "", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, 0);
+    var interpolation = _parseStringInterpolation("'''first\$x'''");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 10);
   }
 
   void test_contentsOffset_singleQuote_multiline_last_unterminated() {
-    var node = interpolationString("foo\n", "foo\n", false, true);
-    expect(node.contentsOffset, 0);
-    expect(node.contentsEnd, "foo\n".length);
+    var interpolation = _parseStringInterpolation("'''first\$x'''");
+    var node = interpolation.lastString;
+    _assertContentsOffsetEnd(node, 10, 10);
+  }
+
+  void _assertContentsOffsetEnd(InterpolationString node, int offset, int end) {
+    expect(node.contentsOffset, _baseOffset + offset);
+    expect(node.contentsEnd, _baseOffset + end);
+  }
+
+  StringInterpolation _parseStringInterpolation(String code) {
+    var unitCode = 'var x = ';
+    _baseOffset = unitCode.length;
+    unitCode += code;
+    var unit = parseString(
+      content: unitCode,
+      throwIfDiagnostics: false,
+    ).unit;
+    var declaration = unit.declarations[0] as TopLevelVariableDeclaration;
+    return declaration.variables.variables[0].initializer
+        as StringInterpolation;
   }
 }
 
@@ -1587,13 +1572,14 @@
 @reflectiveTest
 class StringInterpolationTest extends ParserTestCase {
   void test_contentsOffsetEnd() {
-    AstTestFactory.interpolationExpression(AstTestFactory.identifier3('bb'));
+    var bb = AstTestFactory.interpolationExpression(
+        AstTestFactory.identifier3('bb'));
     // 'a${bb}ccc'
     {
       var ae = AstTestFactory.interpolationString("'a", "a");
       var cToken = StringToken(TokenType.STRING, "ccc'", 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 1);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1602,7 +1588,7 @@
       var ae = AstTestFactory.interpolationString("'''a", "a");
       var cToken = StringToken(TokenType.STRING, "ccc'''", 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 3);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1611,7 +1597,7 @@
       var ae = AstTestFactory.interpolationString('"""a', "a");
       var cToken = StringToken(TokenType.STRING, 'ccc"""', 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 3);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1620,7 +1606,7 @@
       var ae = AstTestFactory.interpolationString("r'a", "a");
       var cToken = StringToken(TokenType.STRING, "ccc'", 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 2);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1629,7 +1615,7 @@
       var ae = AstTestFactory.interpolationString("r'''a", "a");
       var cToken = StringToken(TokenType.STRING, "ccc'''", 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 4);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1638,7 +1624,7 @@
       var ae = AstTestFactory.interpolationString('r"""a', "a");
       var cToken = StringToken(TokenType.STRING, 'ccc"""', 10);
       var cElement = astFactory.interpolationString(cToken, 'ccc');
-      StringInterpolation node = AstTestFactory.string([ae, ae, cElement]);
+      StringInterpolation node = AstTestFactory.string([ae, bb, cElement]);
       expect(node.contentsOffset, 4);
       expect(node.contentsEnd, 10 + 4 - 1);
     }
@@ -1678,7 +1664,7 @@
   }
 
   void test_isRaw() {
-    StringInterpolation node = AstTestFactory.string();
+    var node = parseStringLiteral('"first \$x last"') as StringInterpolation;
     expect(node.isRaw, isFalse);
   }
 
diff --git a/pkg/analyzer/test/generated/utilities_test.dart b/pkg/analyzer/test/generated/utilities_test.dart
index 57f5ce0..60aba02a 100644
--- a/pkg/analyzer/test/generated/utilities_test.dart
+++ b/pkg/analyzer/test/generated/utilities_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/analysis/features.dart';
+import 'package:analyzer/dart/analysis/utilities.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
@@ -3356,8 +3357,10 @@
   }
 
   void test_stringInterpolation() {
-    StringInterpolation node =
-        AstTestFactory.string([AstTestFactory.interpolationExpression2("a")]);
+    var unit = parseString(content: 'var v = "first \$x last";').unit;
+    var declaration = unit.declarations[0] as TopLevelVariableDeclaration;
+    var variable = declaration.variables.variables[0];
+    var node = variable.initializer as StringInterpolation;
     _assertReplace(
         node, ListGetter_NodeReplacerTest_test_stringInterpolation(0));
   }