Version 2.14.0-113.0.dev

Merge commit 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e' into 'dev'
diff --git a/pkg/analysis_server/lib/src/computer/computer_highlights.dart b/pkg/analysis_server/lib/src/computer/computer_highlights.dart
index 0233e2e..1cba6ec 100644
--- a/pkg/analysis_server/lib/src/computer/computer_highlights.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_highlights.dart
@@ -2,6 +2,11 @@
 // 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:math' as math;
+
+import 'package:_fe_analyzer_shared/src/parser/quote.dart'
+    show analyzeQuote, Quote, firstQuoteLength, lastQuoteLength;
+import 'package:_fe_analyzer_shared/src/scanner/characters.dart' as char;
 import 'package:analysis_server/lsp_protocol/protocol_generated.dart'
     show SemanticTokenTypes, SemanticTokenModifiers;
 import 'package:analysis_server/src/lsp/constants.dart'
@@ -953,6 +958,9 @@
   @override
   void visitSimpleStringLiteral(SimpleStringLiteral node) {
     computer._addRegion_node(node, HighlightRegionType.LITERAL_STRING);
+    if (computer._computeSemanticTokens) {
+      _addRegions_stringEscapes(node);
+    }
     super.visitSimpleStringLiteral(node);
   }
 
@@ -1065,4 +1073,98 @@
           semanticTokenModifiers: {CustomSemanticTokenModifiers.control});
     }
   }
+
+  void _addRegions_stringEscapes(SimpleStringLiteral node) {
+    final string = node.literal.lexeme;
+    final quote = analyzeQuote(string);
+    final startIndex = firstQuoteLength(string, quote);
+    final endIndex = string.length - lastQuoteLength(quote);
+    switch (quote) {
+      case Quote.Single:
+      case Quote.Double:
+      case Quote.MultiLineSingle:
+      case Quote.MultiLineDouble:
+        _findEscapes(node, startIndex: startIndex, endIndex: endIndex,
+            listener: (offset, end) {
+          final length = end - offset;
+          computer._addRegion(node.offset + offset, length,
+              HighlightRegionType.VALID_STRING_ESCAPE);
+        });
+        break;
+      case Quote.RawSingle:
+      case Quote.RawDouble:
+      case Quote.RawMultiLineSingle:
+      case Quote.RawMultiLineDouble:
+        // Raw strings don't have escape characters.
+        break;
+    }
+  }
+
+  /// Finds escaped regions within a string between [startIndex] and [endIndex],
+  /// calling [listener] for each found region.
+  void _findEscapes(
+    SimpleStringLiteral node, {
+    required int startIndex,
+    required int endIndex,
+    required void Function(int offset, int end) listener,
+  }) {
+    final string = node.literal.lexeme;
+    final codeUnits = string.codeUnits;
+    final length = string.length;
+
+    bool isBackslash(int i) => i <= length && codeUnits[i] == char.$BACKSLASH;
+    bool isHexEscape(int i) => i <= length && codeUnits[i] == char.$x;
+    bool isUnicodeHexEscape(int i) => i <= length && codeUnits[i] == char.$u;
+    bool isOpenBrace(int i) =>
+        i <= length && codeUnits[i] == char.$OPEN_CURLY_BRACKET;
+    bool isCloseBrace(int i) =>
+        i <= length && codeUnits[i] == char.$CLOSE_CURLY_BRACKET;
+    int? numHexDigits(int i, {required int min, required int max}) {
+      var numHexDigits = 0;
+      for (var j = i; j < math.min(i + max, length); j++) {
+        if (!char.isHexDigit(codeUnits[j])) {
+          break;
+        }
+        numHexDigits++;
+      }
+      return numHexDigits >= min ? numHexDigits : null;
+    }
+
+    for (var i = startIndex; i < endIndex;) {
+      if (isBackslash(i)) {
+        final backslashOffset = i++;
+        // All escaped characters are a single character except for:
+        // `\uXXXX` or `\u{XX?X?X?X?X?}` for Unicode hex escape.
+        // `\xXX` for hex escape.
+        if (isHexEscape(i)) {
+          // Expect exactly 2 hex digits.
+          final numDigits = numHexDigits(i + 1, min: 2, max: 2);
+          if (numDigits != null) {
+            i += 1 + numDigits;
+            listener(backslashOffset, i);
+          }
+        } else if (isUnicodeHexEscape(i) && isOpenBrace(i + 1)) {
+          // Expect 1-6 hex digits followed by '}'.
+          final numDigits = numHexDigits(i + 2, min: 1, max: 6);
+          if (numDigits != null && isCloseBrace(i + 2 + numDigits)) {
+            i += 2 + numDigits + 1;
+            listener(backslashOffset, i);
+          }
+        } else if (isUnicodeHexEscape(i)) {
+          // Expect exactly 4 hex digits.
+          final numDigits = numHexDigits(i + 1, min: 4, max: 4);
+          if (numDigits != null) {
+            i += 1 + numDigits;
+            listener(backslashOffset, i);
+          }
+        } else {
+          i++;
+          // Single-character escape.
+          listener(backslashOffset, i);
+        }
+      } else {
+        i++;
+      }
+    }
+  }
 }
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index 3b44a12..02479bd 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -108,6 +108,10 @@
   /// to class names that are not constructors.
   static const constructor = SemanticTokenModifiers('constructor');
 
+  /// A modifier applied to escape characters within a string to allow colouring
+  /// them differently.
+  static const escape = SemanticTokenModifiers('escape');
+
   /// All custom semantic token modifiers, used to populate the LSP Legend which must
   /// include all used modifiers.
   static const values = [control, label, constructor];
diff --git a/pkg/analysis_server/lib/src/lsp/semantic_tokens/mapping.dart b/pkg/analysis_server/lib/src/lsp/semantic_tokens/mapping.dart
index c83e229..ef2eda3 100644
--- a/pkg/analysis_server/lib/src/lsp/semantic_tokens/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/semantic_tokens/mapping.dart
@@ -75,6 +75,9 @@
   HighlightRegionType.TOP_LEVEL_VARIABLE_DECLARATION: {
     SemanticTokenModifiers.declaration
   },
+  HighlightRegionType.VALID_STRING_ESCAPE: {
+    CustomSemanticTokenModifiers.escape
+  },
 };
 
 /// A mapping from [HighlightRegionType] to [SemanticTokenTypes].
@@ -137,6 +140,7 @@
   HighlightRegionType.TYPE_PARAMETER: SemanticTokenTypes.typeParameter,
   HighlightRegionType.UNRESOLVED_INSTANCE_MEMBER_REFERENCE:
       SemanticTokenTypes.variable,
+  HighlightRegionType.VALID_STRING_ESCAPE: SemanticTokenTypes.string,
 };
 
 /// A helper for converting from Server highlight regions to LSP semantic tokens.
diff --git a/pkg/analysis_server/test/lsp/semantic_tokens_test.dart b/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
index 20d0bfc..107872f 100644
--- a/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
+++ b/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
@@ -857,6 +857,62 @@
     expect(decoded, equals(expected));
   }
 
+  Future<void> test_strings_escape() async {
+    // The 9's in these strings are not part of the escapes (they make the
+    // strings too long).
+    final content = r'''
+const string1 = 'it\'s escaped\\\n';
+const string2 = 'hex \x12\x1299';
+const string3 = 'unicode \u1234\u123499\u{123456}\u{12345699}';
+''';
+
+    final expected = [
+      _Token('const', SemanticTokenTypes.keyword),
+      _Token('string1', SemanticTokenTypes.variable,
+          [SemanticTokenModifiers.declaration]),
+      _Token("'it", SemanticTokenTypes.string),
+      _Token(r"\'", SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      _Token('s escaped', SemanticTokenTypes.string),
+      _Token(r'\\', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      _Token(r'\n', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      _Token(r"'", SemanticTokenTypes.string),
+      _Token('const', SemanticTokenTypes.keyword),
+      _Token('string2', SemanticTokenTypes.variable,
+          [SemanticTokenModifiers.declaration]),
+      _Token("'hex ", SemanticTokenTypes.string),
+      _Token(r'\x12', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      _Token(r'\x12', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      // The 99 is not part of the escape
+      _Token("99'", SemanticTokenTypes.string),
+      _Token('const', SemanticTokenTypes.keyword),
+      _Token('string3', SemanticTokenTypes.variable,
+          [SemanticTokenModifiers.declaration]),
+      _Token("'unicode ", SemanticTokenTypes.string),
+      _Token(r'\u1234', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      _Token(r'\u1234', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      // The 99 is not part of the escape
+      _Token('99', SemanticTokenTypes.string),
+      _Token(r'\u{123456}', SemanticTokenTypes.string,
+          [CustomSemanticTokenModifiers.escape]),
+      // The 99 makes this invalid so i's not an escape
+      _Token(r"\u{12345699}'", SemanticTokenTypes.string),
+    ];
+
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(content));
+
+    final tokens = await getSemanticTokens(mainFileUri);
+    final decoded = decodeSemanticTokens(content, tokens);
+    expect(decoded, equals(expected));
+  }
+
   Future<void> test_topLevel() async {
     final content = '''
     /// strings docs
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 037ae47..90beb07 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -6140,7 +6140,7 @@
     return member is Procedure &&
         !member.isAccessor &&
         !member.isFactory &&
-        !_isInForeignJS &&
+        !(_isInForeignJS && isBuildingSdk) &&
         !usesJSInterop(member) &&
         _reifyFunctionType(member.function);
   }
diff --git a/tests/lib/js/js_util/properties_test.dart b/tests/lib/js/js_util/properties_test.dart
index ab0b844..d21a2c3 100644
--- a/tests/lib/js/js_util/properties_test.dart
+++ b/tests/lib/js/js_util/properties_test.dart
@@ -50,6 +50,11 @@
   external get b;
 }
 
+class ExampleTearoff {
+  int x = 3;
+  foo() => x;
+}
+
 String _getBarWithSideEffect() {
   var x = 5;
   expect(x, equals(5));
@@ -314,6 +319,10 @@
       String bar = _getBarWithSideEffect();
       js_util.setProperty(f, bar, 'baz');
       expect(js_util.getProperty(f, bar), equals('baz'));
+
+      // Using a tearoff as the property value
+      js_util.setProperty(f, 'tearoff', allowInterop(ExampleTearoff().foo));
+      expect(js_util.callMethod(f, 'tearoff', []), equals(3));
     });
   });
 
diff --git a/tests/lib_2/js/js_util/properties_test.dart b/tests/lib_2/js/js_util/properties_test.dart
index 4437065..3d2110b 100644
--- a/tests/lib_2/js/js_util/properties_test.dart
+++ b/tests/lib_2/js/js_util/properties_test.dart
@@ -52,6 +52,11 @@
   external get b;
 }
 
+class ExampleTearoff {
+  int x = 3;
+  foo() => x;
+}
+
 String _getBarWithSideEffect() {
   var x = 5;
   expect(x, equals(5));
@@ -316,6 +321,10 @@
       String bar = _getBarWithSideEffect();
       js_util.setProperty(f, bar, 'baz');
       expect(js_util.getProperty(f, bar), equals('baz'));
+
+      // Using a tearoff as the property value
+      js_util.setProperty(f, 'tearoff', allowInterop(ExampleTearoff().foo));
+      expect(js_util.callMethod(f, 'tearoff', []), equals(3));
     });
   });
 
diff --git a/tools/VERSION b/tools/VERSION
index 4af193d..8dbbac1 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 112
+PRERELEASE 113
 PRERELEASE_PATCH 0
\ No newline at end of file