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