Version 2.15.0-299.0.dev

Merge commit '93ac98d6629f9a6f41e4ee505c054d6076aaae8a' into 'dev'
diff --git a/pkg/analysis_server/lib/src/cider/rename.dart b/pkg/analysis_server/lib/src/cider/rename.dart
index 0aa1a3f..29f89fc 100644
--- a/pkg/analysis_server/lib/src/cider/rename.dart
+++ b/pkg/analysis_server/lib/src/cider/rename.dart
@@ -51,7 +51,7 @@
 
   RenameResponse? computeRenameRanges() {
     var matches = canRename._fileResolver.findReferences(
-        canRename.refactoringElement.offset, canRename.filePath);
+        canRename.refactoringElement.element, canRename.filePath);
     return RenameResponse(matches, this);
   }
 }
diff --git a/pkg/analyzer/lib/error/error.dart b/pkg/analyzer/lib/error/error.dart
index 950eb5b..56a23d3 100644
--- a/pkg/analyzer/lib/error/error.dart
+++ b/pkg/analyzer/lib/error/error.dart
@@ -633,6 +633,8 @@
   HintCode.UNNECESSARY_QUESTION_MARK,
   HintCode.UNNECESSARY_TYPE_CHECK_FALSE,
   HintCode.UNNECESSARY_TYPE_CHECK_TRUE,
+  HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT,
+  HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL,
   HintCode.UNUSED_CATCH_CLAUSE,
   HintCode.UNUSED_CATCH_STACK,
   HintCode.UNUSED_ELEMENT,
diff --git a/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart b/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
index 588e474..076e54f 100644
--- a/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/library_analyzer.dart
@@ -33,6 +33,7 @@
 import 'package:analyzer/src/error/language_version_override_verifier.dart';
 import 'package:analyzer/src/error/override_verifier.dart';
 import 'package:analyzer/src/error/todo_finder.dart';
+import 'package:analyzer/src/error/unicode_text_verifier.dart';
 import 'package:analyzer/src/error/unused_local_elements_verifier.dart';
 import 'package:analyzer/src/generated/declaration_resolver.dart';
 import 'package:analyzer/src/generated/engine.dart';
@@ -348,6 +349,8 @@
       );
     }
 
+    UnicodeTextVerifier(errorReporter).verify(unit, file.content);
+
     unit.accept(DeadCodeVerifier(errorReporter));
 
     unit.accept(
diff --git a/pkg/analyzer/lib/src/dart/error/hint_codes.g.dart b/pkg/analyzer/lib/src/dart/error/hint_codes.g.dart
index 8dc8f2a..ecb67b1 100644
--- a/pkg/analyzer/lib/src/dart/error/hint_codes.g.dart
+++ b/pkg/analyzer/lib/src/dart/error/hint_codes.g.dart
@@ -2914,6 +2914,92 @@
   );
 
   /**
+   * Parameters:
+   * 0: the unicode sequence of the code point.
+   */
+  // #### Description
+  //
+  // The analyzer produces this diagnostic when it encounters source that
+  // contains text direction Unicode code points. These code points cause
+  // source code in either a string literal or a comment to be interpreted
+  // and compiled differently than how it appears in editors, leading to
+  // possible security vulnerabilities.
+  //
+  // #### Example
+  //
+  // The following code produces this diagnostic twice because there are
+  // hidden characters at the start and end of the label string:
+  //
+  // ```dart
+  // var label = '[!I!]nteractive text[!'!];
+  // ```
+  //
+  // #### Common fixes
+  //
+  // If the code points are intended to be included in the string literal,
+  // then escape them:
+  //
+  // ```dart
+  // var label = '\u202AInteractive text\u202C';
+  // ```
+  //
+  // If the code points aren't intended to be included in the string literal,
+  // then remove them:
+  //
+  // ```dart
+  // var label = 'Interactive text';
+  // ```
+  static const HintCode TEXT_DIRECTION_CODE_POINT_IN_COMMENT = HintCode(
+    'TEXT_DIRECTION_CODE_POINT_IN_COMMENT',
+    "The Unicode code point 'U+{0}' changes the appearance of text from how it's interpreted by the compiler.",
+    correctionMessage:
+        "Try removing the code point or using the Unicode escape sequence '\\u{0}'.",
+  );
+
+  /**
+   * Parameters:
+   * 0: the unicode sequence of the code point.
+   */
+  // #### Description
+  //
+  // The analyzer produces this diagnostic when it encounters source that
+  // contains text direction Unicode code points. These code points cause
+  // source code in either a string literal or a comment to be interpreted
+  // and compiled differently than how it appears in editors, leading to
+  // possible security vulnerabilities.
+  //
+  // #### Example
+  //
+  // The following code produces this diagnostic twice because there are
+  // hidden characters at the start and end of the label string:
+  //
+  // ```dart
+  // var label = '[!I!]nteractive text[!'!];
+  // ```
+  //
+  // #### Common fixes
+  //
+  // If the code points are intended to be included in the string literal,
+  // then escape them:
+  //
+  // ```dart
+  // var label = '\u202AInteractive text\u202C';
+  // ```
+  //
+  // If the code points aren't intended to be included in the string literal,
+  // then remove them:
+  //
+  // ```dart
+  // var label = 'Interactive text';
+  // ```
+  static const HintCode TEXT_DIRECTION_CODE_POINT_IN_LITERAL = HintCode(
+    'TEXT_DIRECTION_CODE_POINT_IN_LITERAL',
+    "The Unicode code point 'U+{0}' changes the appearance of text from how it's interpreted by the compiler.",
+    correctionMessage:
+        "Try removing the code point or using the Unicode escape sequence '\\u{0}'.",
+  );
+
+  /**
    * No parameters.
    */
   // #### Description
diff --git a/pkg/analyzer/lib/src/dart/micro/library_analyzer.dart b/pkg/analyzer/lib/src/dart/micro/library_analyzer.dart
index be5b2ba..e904ee0 100644
--- a/pkg/analyzer/lib/src/dart/micro/library_analyzer.dart
+++ b/pkg/analyzer/lib/src/dart/micro/library_analyzer.dart
@@ -31,6 +31,7 @@
 import 'package:analyzer/src/error/inheritance_override.dart';
 import 'package:analyzer/src/error/override_verifier.dart';
 import 'package:analyzer/src/error/todo_finder.dart';
+import 'package:analyzer/src/error/unicode_text_verifier.dart';
 import 'package:analyzer/src/error/unused_local_elements_verifier.dart';
 import 'package:analyzer/src/generated/declaration_resolver.dart';
 import 'package:analyzer/src/generated/engine.dart';
@@ -270,6 +271,8 @@
     unit.accept(DeadCodeVerifier(errorReporter));
 
     var content = getFileContent(file);
+    UnicodeTextVerifier(errorReporter).verify(unit, content);
+
     unit.accept(
       BestPracticesVerifier(
         errorReporter,
diff --git a/pkg/analyzer/lib/src/dart/micro/resolve_file.dart b/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
index b9fb939..24876d2 100644
--- a/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
+++ b/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
@@ -18,7 +18,6 @@
 import 'package:analyzer/src/dart/analysis/feature_set_provider.dart';
 import 'package:analyzer/src/dart/analysis/performance_logger.dart';
 import 'package:analyzer/src/dart/analysis/results.dart';
-import 'package:analyzer/src/dart/ast/utilities.dart';
 import 'package:analyzer/src/dart/micro/analysis_context.dart';
 import 'package:analyzer/src/dart/micro/cider_byte_store.dart';
 import 'package:analyzer/src/dart/micro/library_analyzer.dart';
@@ -186,30 +185,25 @@
     removedCacheIds.addAll(libraryContext!.collectSharedDataIdentifiers());
   }
 
-  /// Looks for references to the Element at the given offset and path. All the
+  /// Looks for references to the given Element in the path. All the
   /// files currently cached by the resolver are searched, generated files are
   /// ignored.
-  List<CiderSearchMatch> findReferences(int offset, String path,
+  List<CiderSearchMatch> findReferences(Element element, String path,
       {OperationPerformanceImpl? performance}) {
     var references = <CiderSearchMatch>[];
-    var unit = resolve(path: path);
-    var node = NodeLocator(offset).searchWithin(unit.unit);
-    var element = getElementOfNode(node);
-    if (element != null) {
-      // TODO(keertip): check if element is named constructor.
-      var result = fsState!.getFilesContaining(element.displayName);
-      result.forEach((filePath) {
-        var resolved = resolve(path: filePath);
-        var collector = ReferencesCollector(element);
-        resolved.unit.accept(collector);
-        var offsets = collector.offsets;
-        if (offsets.isNotEmpty) {
-          var lineInfo = resolved.unit.lineInfo;
-          references.add(CiderSearchMatch(filePath,
-              offsets.map((offset) => lineInfo?.getLocation(offset)).toList()));
-        }
-      });
-    }
+    // TODO(keertip): check if element is named constructor.
+    var result = fsState!.getFilesContaining(element.displayName);
+    result.forEach((filePath) {
+      var resolved = resolve(path: filePath);
+      var collector = ReferencesCollector(element);
+      resolved.unit.accept(collector);
+      var offsets = collector.offsets;
+      if (offsets.isNotEmpty) {
+        var lineInfo = resolved.unit.lineInfo;
+        references.add(CiderSearchMatch(filePath,
+            offsets.map((offset) => lineInfo?.getLocation(offset)).toList()));
+      }
+    });
     return references;
   }
 
diff --git a/pkg/analyzer/lib/src/error/unicode_text_verifier.dart b/pkg/analyzer/lib/src/error/unicode_text_verifier.dart
new file mode 100644
index 0000000..cdbe7ad
--- /dev/null
+++ b/pkg/analyzer/lib/src/error/unicode_text_verifier.dart
@@ -0,0 +1,37 @@
+// 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/src/dart/ast/utilities.dart';
+import 'package:analyzer/src/error/codes.dart';
+
+/// A verifier that checks for unsafe Unicode text.
+/// todo(pq): update w/ a Dart CVE link once published
+class UnicodeTextVerifier {
+  final ErrorReporter errorReporter;
+  UnicodeTextVerifier(this.errorReporter);
+
+  void verify(CompilationUnit unit, String source) {
+    for (var offset = 0; offset < source.length; ++offset) {
+      var codeUnit = source.codeUnitAt(offset);
+      // U+202A, U+202B, U+202C, U+202D, U+202E, U+2066, U+2067, U+2068, U+2069.
+      if (0x202a <= codeUnit &&
+          codeUnit <= 0x2069 &&
+          (codeUnit <= 0x202e || 0x2066 <= codeUnit)) {
+        // This uses an AST visitor; consider a more direct approach.
+        var node = NodeLocator(offset).searchWithin(unit);
+        // If it's not in a string literal, we assume we're in a comment.
+        // This can potentially over-report on syntactically incorrect sources
+        // (where Unicode is outside a string or comment).
+        var errorCode =
+            node is SimpleStringLiteral || node is InterpolationString
+                ? HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL
+                : HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT;
+        var code = codeUnit.toRadixString(16).toUpperCase();
+        errorReporter.reportErrorForOffset(errorCode, offset, 1, [code]);
+      }
+    }
+  }
+}
diff --git a/pkg/analyzer/messages.yaml b/pkg/analyzer/messages.yaml
index b1868a7..4c8327f 100644
--- a/pkg/analyzer/messages.yaml
+++ b/pkg/analyzer/messages.yaml
@@ -16459,6 +16459,86 @@
       If the class needs to be a subtype of the sealed class, then either change
       the sealed class so that it's no longer sealed or move the subclass into
       the same package as the sealed class.
+  TEXT_DIRECTION_CODE_POINT_IN_COMMENT:
+    problemMessage: The Unicode code point 'U+{0}' changes the appearance of text from how it's interpreted by the compiler.
+    correctionMessage: Try removing the code point or using the Unicode escape sequence '\u{0}'.
+    hasPublishedDocs: false
+    comment: |-
+      Parameters:
+      0: the unicode sequence of the code point.
+    documentation: |-
+      #### Description
+
+      The analyzer produces this diagnostic when it encounters source that
+      contains text direction Unicode code points. These code points cause
+      source code in either a string literal or a comment to be interpreted
+      and compiled differently than how it appears in editors, leading to
+      possible security vulnerabilities.
+
+      #### Example
+
+      The following code produces this diagnostic twice because there are
+      hidden characters at the start and end of the label string:
+
+      ```dart
+      var label = '[!I!]nteractive text[!'!];
+      ```
+
+      #### Common fixes
+
+      If the code points are intended to be included in the string literal,
+      then escape them:
+
+      ```dart
+      var label = '\u202AInteractive text\u202C';
+      ```
+
+      If the code points aren't intended to be included in the string literal,
+      then remove them:
+
+      ```dart
+      var label = 'Interactive text';
+      ```
+  TEXT_DIRECTION_CODE_POINT_IN_LITERAL:
+    problemMessage: The Unicode code point 'U+{0}' changes the appearance of text from how it's interpreted by the compiler.
+    correctionMessage: Try removing the code point or using the Unicode escape sequence '\u{0}'.
+    hasPublishedDocs: false
+    comment: |-
+      Parameters:
+      0: the unicode sequence of the code point.
+    documentation: |-
+      #### Description
+
+      The analyzer produces this diagnostic when it encounters source that
+      contains text direction Unicode code points. These code points cause
+      source code in either a string literal or a comment to be interpreted
+      and compiled differently than how it appears in editors, leading to
+      possible security vulnerabilities.
+
+      #### Example
+
+      The following code produces this diagnostic twice because there are
+      hidden characters at the start and end of the label string:
+
+      ```dart
+      var label = '[!I!]nteractive text[!'!];
+      ```
+
+      #### Common fixes
+
+      If the code points are intended to be included in the string literal,
+      then escape them:
+
+      ```dart
+      var label = '\u202AInteractive text\u202C';
+      ```
+
+      If the code points aren't intended to be included in the string literal,
+      then remove them:
+
+      ```dart
+      var label = 'Interactive text';
+      ```
   TYPE_CHECK_IS_NOT_NULL:
     sharedName: TYPE_CHECK_WITH_NULL
     problemMessage: "Tests for non-null should be done with '!= null'."
diff --git a/pkg/analyzer/test/src/dart/micro/simple_file_resolver_test.dart b/pkg/analyzer/test/src/dart/micro/simple_file_resolver_test.dart
index 52861ce..3b59012 100644
--- a/pkg/analyzer/test/src/dart/micro/simple_file_resolver_test.dart
+++ b/pkg/analyzer/test/src/dart/micro/simple_file_resolver_test.dart
@@ -2,11 +2,14 @@
 // 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:analyzer/dart/element/element.dart';
 import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer/src/dart/ast/utilities.dart';
 import 'package:analyzer/src/dart/error/syntactic_errors.dart';
 import 'package:analyzer/src/dart/micro/cider_byte_store.dart';
 import 'package:analyzer/src/dart/micro/library_graph.dart';
 import 'package:analyzer/src/dart/micro/resolve_file.dart';
+import 'package:analyzer/src/dart/micro/utils.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/lint/registry.dart';
 import 'package:test/test.dart';
@@ -391,7 +394,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(6, aPath);
+    var result = fileResolver.findReferences(_findElement(6, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(4, 11)]),
       CiderSearchMatch(aPath, [CharacterLocation(1, 7)])
@@ -412,7 +415,7 @@
 ''');
 
     await resolveFile(aPath);
-    var result = fileResolver.findReferences(16, aPath);
+    var result = fileResolver.findReferences(_findElement(16, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(
           aPath, [CharacterLocation(2, 7), CharacterLocation(5, 5)])
@@ -431,7 +434,7 @@
 ''');
 
     await resolveFile(aPath);
-    var result = fileResolver.findReferences(11, aPath);
+    var result = fileResolver.findReferences(_findElement(11, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(
           aPath, [CharacterLocation(2, 3), CharacterLocation(5, 1)])
@@ -457,7 +460,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(20, aPath);
+    var result = fileResolver.findReferences(_findElement(20, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(5, 15)]),
       CiderSearchMatch(aPath, [CharacterLocation(2, 11)])
@@ -476,7 +479,7 @@
 }
 ''');
     await resolveFile(aPath);
-    var result = fileResolver.findReferences(39, aPath);
+    var result = fileResolver.findReferences(_findElement(39, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(
           aPath, [CharacterLocation(3, 9), CharacterLocation(4, 11)])
@@ -509,7 +512,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(17, aPath);
+    var result = fileResolver.findReferences(_findElement(17, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(5, 5)]),
       CiderSearchMatch(
@@ -536,7 +539,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(21, aPath);
+    var result = fileResolver.findReferences(_findElement(21, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(5, 5)]),
       CiderSearchMatch(aPath, [CharacterLocation(2, 12)])
@@ -563,7 +566,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(19, aPath);
+    var result = fileResolver.findReferences(_findElement(19, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(4, 13)]),
       CiderSearchMatch(aPath, [CharacterLocation(3, 9)])
@@ -590,7 +593,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(20, aPath);
+    var result = fileResolver.findReferences(_findElement(20, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(4, 3)]),
       CiderSearchMatch(aPath, [CharacterLocation(3, 10)])
@@ -610,7 +613,7 @@
 ''');
 
     await resolveFile(aPath);
-    var result = fileResolver.findReferences(10, aPath);
+    var result = fileResolver.findReferences(_findElement(10, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(
           aPath, [CharacterLocation(1, 11), CharacterLocation(4, 11)])
@@ -628,7 +631,7 @@
 }
 ''');
     await resolveFile(aPath);
-    var result = fileResolver.findReferences(10, aPath);
+    var result = fileResolver.findReferences(_findElement(10, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(aPath, [
         CharacterLocation(1, 11),
@@ -656,7 +659,7 @@
 ''');
 
     await resolveFile(bPath);
-    var result = fileResolver.findReferences(8, aPath);
+    var result = fileResolver.findReferences(_findElement(8, aPath), aPath);
     var expected = <CiderSearchMatch>[
       CiderSearchMatch(bPath, [CharacterLocation(3, 8)]),
       CiderSearchMatch(aPath, [CharacterLocation(1, 9)])
@@ -1276,4 +1279,11 @@
   void _assertRemovedPaths(Matcher matcher) {
     expect(fileResolver.fsState!.testView.removedPaths, matcher);
   }
+
+  Element _findElement(int offset, String filePath) {
+    var resolvedUnit = fileResolver.resolve(path: filePath);
+    var node = NodeLocator(offset).searchWithin(resolvedUnit.unit);
+    var element = getElementOfNode(node);
+    return element!;
+  }
 }
diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart
index 5b02fd3..779326c 100644
--- a/pkg/analyzer/test/src/diagnostics/test_all.dart
+++ b/pkg/analyzer/test/src/diagnostics/test_all.dart
@@ -631,6 +631,7 @@
     as switch_expression_not_assignable;
 import 'tearoff_of_generative_constructor_of_abstract_class_test.dart'
     as tearoff_of_generative_constructor_of_abstract_class;
+import 'text_direction_code_point_test.dart' as text_direction_code_point;
 import 'throw_of_invalid_type_test.dart' as throw_of_invalid_type;
 import 'todo_test.dart' as todo_test;
 import 'top_level_cycle_test.dart' as top_level_cycle;
@@ -1134,6 +1135,7 @@
     switch_case_completes_normally.main();
     switch_expression_not_assignable.main();
     tearoff_of_generative_constructor_of_abstract_class.main();
+    text_direction_code_point.main();
     throw_of_invalid_type.main();
     todo_test.main();
     top_level_cycle.main();
diff --git a/pkg/analyzer/test/src/diagnostics/text_direction_code_point_test.dart b/pkg/analyzer/test/src/diagnostics/text_direction_code_point_test.dart
new file mode 100644
index 0000000..be19a86
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/text_direction_code_point_test.dart
@@ -0,0 +1,92 @@
+// 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:analyzer/src/dart/error/hint_codes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(UnsafeTextDirectionCodepointTest);
+  });
+}
+
+@reflectiveTest
+class UnsafeTextDirectionCodepointTest extends PubPackageResolutionTest {
+  test_comments() async {
+    await assertErrorsInCode('''
+// \u2066
+/// \u2066
+void f() { // \u2066
+  // \u2066
+}
+''', [
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT, 3, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT, 9, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT, 25, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT, 32, 1),
+    ]);
+  }
+
+  /// https://github.com/flutter/flutter/pull/93029
+  test_file_ok() async {
+    // Raw strings preserve the escapes.
+    await assertNoErrorsInCode(r'''
+var u202a = '\u202AInteractive text\u202C';
+''');
+  }
+
+  test_message_escape() async {
+    await assertErrorsInCode('''
+var u202a = '\u202A';
+''', [
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 13, 1,
+          messageContains: ['U+202A']),
+    ]);
+  }
+
+  test_multiLineString() async {
+    await assertErrorsInCode('''
+var s = """ \u202a
+        Multiline!
+""";
+''', [
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 12, 1),
+    ]);
+  }
+
+  test_simpleStrings() async {
+    await assertErrorsInCode('''
+var u202a = '\u202A';
+var u202b = '\u202B';
+var u202c = '\u202C';
+var u202d = '\u202D';
+var u202e = '\u202E';
+var u2066 = '\u2066';
+var u2067 = '\u2067';
+var u2068 = '\u2068';
+var u2069 = '\u2069';
+''', [
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 13, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 30, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 47, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 64, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 81, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 98, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 115, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 132, 1),
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 149, 1),
+    ]);
+  }
+
+  test_stringInterpolation() async {
+    await assertErrorsInCode('''
+var x = 'x';
+var u202a = '\u202A\$x';
+''', [
+      error(HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL, 26, 1),
+    ]);
+  }
+}
diff --git a/pkg/analyzer/test/verify_diagnostics_test.dart b/pkg/analyzer/test/verify_diagnostics_test.dart
index bb0643d..d5ee1bf 100644
--- a/pkg/analyzer/test/verify_diagnostics_test.dart
+++ b/pkg/analyzer/test/verify_diagnostics_test.dart
@@ -82,6 +82,11 @@
     'CompileTimeErrorCode.YIELD_EACH_IN_NON_GENERATOR',
     // The code has been replaced but is not yet removed.
     'HintCode.DEPRECATED_MEMBER_USE',
+    // Produces more than one error range by design.
+    // TODO: update verification to allow for multiple highlight ranges.
+    'HintCode.TEXT_DIRECTION_CODE_POINT_IN_COMMENT',
+    // Produces more than one error range by design.
+    'HintCode.TEXT_DIRECTION_CODE_POINT_IN_LITERAL',
     // Produces two diagnostics when it should only produce one (see
     // https://github.com/dart-lang/sdk/issues/43051)
     'HintCode.UNNECESSARY_NULL_COMPARISON_FALSE',
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index ba40e44..74533fb 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -13019,6 +13019,82 @@
 
 Tear off the constructor of a concrete class.
 
+### text_direction_code_point_in_comment
+
+_The Unicode code point 'U+{0}' changes the appearance of text from how it's
+interpreted by the compiler._
+
+#### Description
+
+The analyzer produces this diagnostic when it encounters source that
+contains text direction Unicode code points. These code points cause
+source code in either a string literal or a comment to be interpreted
+and compiled differently than how it appears in editors, leading to
+possible security vulnerabilities.
+
+#### Example
+
+The following code produces this diagnostic twice because there are
+hidden characters at the start and end of the label string:
+
+{% prettify dart tag=pre+code %}
+var label = '[!I!]nteractive text[!'!];
+{% endprettify %}
+
+#### Common fixes
+
+If the code points are intended to be included in the string literal,
+then escape them:
+
+{% prettify dart tag=pre+code %}
+var label = '\u202AInteractive text\u202C';
+{% endprettify %}
+
+If the code points aren't intended to be included in the string literal,
+then remove them:
+
+{% prettify dart tag=pre+code %}
+var label = 'Interactive text';
+{% endprettify %}
+
+### text_direction_code_point_in_literal
+
+_The Unicode code point 'U+{0}' changes the appearance of text from how it's
+interpreted by the compiler._
+
+#### Description
+
+The analyzer produces this diagnostic when it encounters source that
+contains text direction Unicode code points. These code points cause
+source code in either a string literal or a comment to be interpreted
+and compiled differently than how it appears in editors, leading to
+possible security vulnerabilities.
+
+#### Example
+
+The following code produces this diagnostic twice because there are
+hidden characters at the start and end of the label string:
+
+{% prettify dart tag=pre+code %}
+var label = '[!I!]nteractive text[!'!];
+{% endprettify %}
+
+#### Common fixes
+
+If the code points are intended to be included in the string literal,
+then escape them:
+
+{% prettify dart tag=pre+code %}
+var label = '\u202AInteractive text\u202C';
+{% endprettify %}
+
+If the code points aren't intended to be included in the string literal,
+then remove them:
+
+{% prettify dart tag=pre+code %}
+var label = 'Interactive text';
+{% endprettify %}
+
 ### throw_of_invalid_type
 
 _The type '{0}' of the thrown expression must be assignable to 'Object'._
diff --git a/tools/VERSION b/tools/VERSION
index bd7b815..dc9f248 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 298
+PRERELEASE 299
 PRERELEASE_PATCH 0
\ No newline at end of file