Version 2.12.0-211.0.dev

Merge commit 'f9c3c09a9e745e2a261529a2d4fba1dd0b01a25e' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index 61d79bc..ddcd0ff 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -49,6 +49,7 @@
 import 'package:analysis_server/src/services/correction/dart/remove_this_expression.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_type_annotation.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_new.dart';
+import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_string_interpolation.dart';
 import 'package:analysis_server/src/services/correction/dart/rename_to_camel_case.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_cascade_with_dot.dart';
 import 'package:analysis_server/src/services/correction/dart/replace_colon_with_equals.dart';
@@ -254,6 +255,9 @@
     LintNames.unnecessary_overrides: [
       RemoveMethodDeclaration.newInstance,
     ],
+    LintNames.unnecessary_string_interpolations: [
+      RemoveUnnecessaryStringInterpolation.newInstance,
+    ],
     LintNames.unnecessary_this: [
       RemoveThisExpression.newInstance,
     ],
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_string_interpolation.dart b/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_string_interpolation.dart
new file mode 100644
index 0000000..31692a1
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_string_interpolation.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2021, 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 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/correction/util.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+class RemoveUnnecessaryStringInterpolation extends CorrectionProducer {
+  @override
+  FixKind get fixKind => DartFixKind.REMOVE_UNNECESSARY_STRING_INTERPOLATION;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    final interpolation = node;
+    if (interpolation is StringInterpolation) {
+      final open = interpolation.elements[0] as InterpolationString;
+      final contents = interpolation.elements[1] as InterpolationExpression;
+      final close = interpolation.elements[2] as InterpolationString;
+
+      await builder.addDartFileEdit(file, (builder) {
+        final expression = contents.expression;
+        if (getExpressionPrecedence(expression) <
+            getExpressionParentPrecedence(interpolation)) {
+          builder.addReplacement(range.startStart(open, expression), (builder) {
+            builder.write('(');
+          });
+          builder.addReplacement(range.endEnd(expression, close), (builder) {
+            builder.write(')');
+          });
+        } else {
+          builder.addDeletion(range.startStart(open, expression));
+          builder.addDeletion(range.endEnd(expression, close));
+        }
+      });
+    }
+  }
+
+  /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+  static RemoveUnnecessaryStringInterpolation newInstance() =>
+      RemoveUnnecessaryStringInterpolation();
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index ec4fa0c..23e7182 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -405,6 +405,12 @@
       50,
       'Remove unnecessary parentheses',
       appliedTogetherMessage: 'Remove all unnecessary parentheses in file');
+  static const REMOVE_UNNECESSARY_STRING_INTERPOLATION = FixKind(
+      'dart.fix.remove.unnecessaryStringInterpolation',
+      50,
+      'Remove unnecessary string interpolation',
+      appliedTogetherMessage:
+          'Remove all unnecessary string interpolations in file');
   static const REMOVE_UNUSED_CATCH_CLAUSE = FixKind(
       'dart.fix.remove.unusedCatchClause', 50, "Remove unused 'catch' clause");
   static const REMOVE_UNUSED_CATCH_STACK = FixKind(
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index 7d7ee48..7cd30ff 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -112,6 +112,7 @@
 import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_cast.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_new.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_parentheses.dart';
+import 'package:analysis_server/src/services/correction/dart/remove_unnecessary_string_interpolation.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unused.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unused_catch_clause.dart';
 import 'package:analysis_server/src/services/correction/dart/remove_unused_catch_stack.dart';
@@ -482,6 +483,9 @@
     LintNames.unnecessary_parenthesis: [
       RemoveUnnecessaryParentheses.newInstance,
     ],
+    LintNames.unnecessary_string_interpolations: [
+      RemoveUnnecessaryStringInterpolation.newInstance,
+    ],
     LintNames.unnecessary_this: [
       RemoveThisExpression.newInstance,
     ],
diff --git a/pkg/analysis_server/lib/src/services/linter/lint_names.dart b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
index 48910dc..767aabb 100644
--- a/pkg/analysis_server/lib/src/services/linter/lint_names.dart
+++ b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
@@ -96,6 +96,8 @@
       'unnecessary_null_in_if_null_operators';
   static const String unnecessary_overrides = 'unnecessary_overrides';
   static const String unnecessary_parenthesis = 'unnecessary_parenthesis';
+  static const String unnecessary_string_interpolations =
+      'unnecessary_string_interpolations';
   static const String unnecessary_this = 'unnecessary_this';
   static const String use_full_hex_values_for_flutter_colors =
       'use_full_hex_values_for_flutter_colors';
diff --git a/pkg/analysis_server/test/src/services/correction/fix/bulk/remove_unnecessary_string_interpolation_test.dart b/pkg/analysis_server/test/src/services/correction/fix/bulk/remove_unnecessary_string_interpolation_test.dart
new file mode 100644
index 0000000..1ae3d1d
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/bulk/remove_unnecessary_string_interpolation_test.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2021, 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 'package:analysis_server/src/services/linter/lint_names.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'bulk_fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(RemoveUnnecessaryStringInterpolation);
+  });
+}
+
+@reflectiveTest
+class RemoveUnnecessaryStringInterpolation extends BulkFixProcessorTest {
+  @override
+  String get lintCode => LintNames.unnecessary_string_interpolations;
+
+  Future<void> test_embedded_removeBoth() async {
+    await resolveTestCode(r'''
+void f(String s) {
+  print('${'$s'}');
+}
+''');
+    await assertHasFix(r'''
+void f(String s) {
+  print(s);
+}
+''');
+  }
+
+  Future<void> test_embedded_removeOuter() async {
+    await resolveTestCode(r'''
+void f(String s) {
+  print('${'$s '}');
+}
+''');
+    await assertHasFix(r'''
+void f(String s) {
+  print('$s ');
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/bulk/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/bulk/test_all.dart
index a8cc4c4..d7f62b78 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/bulk/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/bulk/test_all.dart
@@ -52,6 +52,8 @@
 import 'remove_type_annotation_test.dart' as remove_type_annotation;
 import 'remove_unnecessary_const_test.dart' as remove_unnecessary_const;
 import 'remove_unnecessary_new_test.dart' as remove_unnecessary_new;
+import 'remove_unnecessary_string_interpolation_test.dart'
+    as remove_unnecessary_string_interpolation;
 import 'rename_to_camel_case_test.dart' as rename_to_camel_case;
 import 'replace_colon_with_equals_test.dart' as replace_colon_with_equals;
 import 'replace_final_with_const_test.dart' as replace_final_with_const;
@@ -111,6 +113,7 @@
     remove_type_annotation.main();
     remove_unnecessary_const.main();
     remove_unnecessary_new.main();
+    remove_unnecessary_string_interpolation.main();
     rename_to_camel_case.main();
     replace_with_conditional_assignment.main();
     replace_colon_with_equals.main();
diff --git a/pkg/analysis_server/test/src/services/correction/fix/remove_unnecessary_string_interpolation_test.dart b/pkg/analysis_server/test/src/services/correction/fix/remove_unnecessary_string_interpolation_test.dart
new file mode 100644
index 0000000..cd6a3c4
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/remove_unnecessary_string_interpolation_test.dart
@@ -0,0 +1,213 @@
+// Copyright (c) 2021, 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 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/linter/lint_names.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(RemoveUnnecessaryStringInterpolationTest);
+  });
+}
+
+@reflectiveTest
+class RemoveUnnecessaryStringInterpolationTest extends FixProcessorLintTest {
+  @override
+  FixKind get kind => DartFixKind.REMOVE_UNNECESSARY_STRING_INTERPOLATION;
+
+  @override
+  String get lintCode => LintNames.unnecessary_string_interpolations;
+
+  Future<void> test_doubleQuote_noBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  "$test";
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_doubleQuote_withBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  "${test}";
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_maintainPrecedence_hasParentheses() async {
+    await resolveTestCode(r'''
+void f(String s) {
+  print('${(s * 2)}'.length);
+}
+''');
+    await assertHasFix(r'''
+void f(String s) {
+  print((s * 2).length);
+}
+''');
+  }
+
+  Future<void> test_maintainPrecedence_noParentheses() async {
+    await resolveTestCode(r'''
+void f(String s) {
+  print('${s * 2}'.length);
+}
+''');
+    await assertHasFix(r'''
+void f(String s) {
+  print((s * 2).length);
+}
+''');
+  }
+
+  Future<void> test_maintainPrecedence_unaryPrefix_addParentheses() async {
+    await resolveTestCode(r'''
+extension Minusable on String {
+  String operator -() => '-$this';
+}
+
+void f(String s) {
+  print('${-  s}'.length);
+}
+''');
+    await assertHasFix(r'''
+extension Minusable on String {
+  String operator -() => '-$this';
+}
+
+void f(String s) {
+  print((-  s).length);
+}
+''');
+  }
+
+  Future<void> test_maintainPrecedence_unaryPrefix_doNotAddParentheses() async {
+    await resolveTestCode(r'''
+extension Minusable on String {
+  String operator -() => '-$this';
+}
+
+void f(String s) {
+  print('${-s}');
+}
+''');
+    await assertHasFix(r'''
+extension Minusable on String {
+  String operator -() => '-$this';
+}
+
+void f(String s) {
+  print(-s);
+}
+''');
+  }
+
+  Future<void> test_singleQuote_noBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  '$test';
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_singleQuote_withBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  '${test}';
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_tripleDoubleQuote_noBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  """$test""";
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_tripleDoubleQuote_withBrackets() async {
+    await resolveTestCode(r'''
+void f() {
+  const test = 'testing';
+  """${test}""";
+}
+''');
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_tripleSingleQuote_noBrackets() async {
+    await resolveTestCode(r"""
+void f() {
+  const test = 'testing';
+  '''$test''';
+}
+""");
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+
+  Future<void> test_tripleSingleQuote_withBrackets() async {
+    await resolveTestCode(r"""
+void f() {
+  const test = 'testing';
+  '''${test}''';
+}
+""");
+    await assertHasFix(r'''
+void f() {
+  const test = 'testing';
+  test;
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index fc73623..f4b05e6 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -130,6 +130,8 @@
 import 'remove_unnecessary_new_test.dart' as remove_unnecessary_new;
 import 'remove_unnecessary_parentheses_test.dart'
     as remove_unnecessary_parentheses;
+import 'remove_unnecessary_string_interpolation_test.dart'
+    as remove_unnecessary_string_interpolation;
 import 'remove_unused_catch_clause_test.dart' as remove_unused_catch_clause;
 import 'remove_unused_catch_stack_test.dart' as remove_unused_catch_stack;
 import 'remove_unused_element_test.dart' as remove_unused_element;
@@ -286,6 +288,7 @@
     remove_unnecessary_const.main();
     remove_unnecessary_new.main();
     remove_unnecessary_parentheses.main();
+    remove_unnecessary_string_interpolation.main();
     remove_unused_catch_clause.main();
     remove_unused_catch_stack.main();
     remove_unused_element.main();
diff --git a/tools/VERSION b/tools/VERSION
index ea33b59..5222bce 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 12
 PATCH 0
-PRERELEASE 210
+PRERELEASE 211
 PRERELEASE_PATCH 0
\ No newline at end of file