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));
}