Version 2.10.0-71.0.dev

Merge commit '6fcd615a26110ddc8688d8c51a16e6e32cc9a861' into 'dev'
diff --git a/pkg/analyzer/lib/src/manifest/manifest_validator.dart b/pkg/analyzer/lib/src/manifest/manifest_validator.dart
index 820e1b2..0096f76 100644
--- a/pkg/analyzer/lib/src/manifest/manifest_validator.dart
+++ b/pkg/analyzer/lib/src/manifest/manifest_validator.dart
@@ -5,13 +5,384 @@
 import 'package:analyzer/error/error.dart';
 import 'package:analyzer/error/listener.dart';
 import 'package:analyzer/src/generated/source.dart';
-import 'package:html/dom.dart';
-import 'package:html/parser.dart' show parseFragment;
+import 'package:charcode/charcode.dart';
+import 'package:meta/meta.dart';
 import 'package:source_span/source_span.dart';
 
 import 'manifest_values.dart';
 import 'manifest_warning_code.dart';
 
+/// A rudimentary parser for Android Manifest files.
+///
+/// Android Manifest files are written in XML. In order to validate an Android
+/// Manifest file, however, we do not need to parse or retain each element. This
+/// parser understands which elements are relevant to manifest validation.
+/// This parser does not validate the XML, and if it encounters an error while
+/// parsing, no exception is thrown. Instead, a parse result with
+/// [ParseResult.error] is returned.
+///
+/// This parser does not understand
+///
+/// * CDATA sections (https://www.w3.org/TR/xml/#sec-cdata-sect),
+/// * element type declarations (https://www.w3.org/TR/xml/#elemdecls),
+/// * attribute list declarations (https://www.w3.org/TR/xml/#attdecls),
+/// * conditional sections (https://www.w3.org/TR/xml/#sec-condition-sect),
+/// * entity declarations (https://www.w3.org/TR/xml/#sec-entity-decl),
+/// * notation declarations (https://www.w3.org/TR/xml/#Notations).
+///
+/// This parser does not replace character or entity references
+/// (https://www.w3.org/TR/xml/#sec-references).
+class ManifestParser {
+  /// Elements which are relevant to manifest validation.
+  static const List<String> _relevantElements = [
+    ACTIVITY_TAG,
+    APPLICATION_TAG,
+    MANIFEST_TAG,
+    USES_FEATURE_TAG,
+    USES_PERMISSION_TAG
+  ];
+
+  /// The text of the Android Manifest file.
+  final String content;
+
+  /// The source file representing the Android Manifest file, for source span
+  /// purposes.
+  final SourceFile sourceFile;
+
+  /// The current offset in the source file.
+  int _pos;
+
+  ManifestParser(this.content, Uri uri)
+      : sourceFile = SourceFile.fromString(content, url: uri),
+        _pos = 0;
+
+  /// Whether the current character is a tag-closing character (">").
+  bool get _isClosing =>
+      _pos < content.length && content.codeUnitAt(_pos) == $gt;
+
+  /// Whether the current character and the following two characters make a
+  /// comment closing ("-->").
+  bool get _isCommentClosing =>
+      _pos + 2 < content.length &&
+      content.codeUnitAt(_pos) == $dash &&
+      content.codeUnitAt(_pos + 1) == $dash &&
+      content.codeUnitAt(_pos + 2) == $gt;
+
+  /// Whether the following three characters make a comment opening ("<!--").
+  bool get _isCommentOpening =>
+      _pos + 3 < content.length &&
+      content.codeUnitAt(_pos + 1) == $exclamation &&
+      content.codeUnitAt(_pos + 2) == $dash &&
+      content.codeUnitAt(_pos + 3) == $dash;
+
+  /// Whether the following character makes a comment opening ("<!").
+  bool get _isDeclarationOpening =>
+      _pos + 1 < content.length && content.codeUnitAt(_pos + 1) == $exclamation;
+
+  /// Whether the current character and the following character make a
+  /// two-character closing.
+  ///
+  /// The "/>" and "?>" closings each represent an empty element.
+  bool get _isTwoCharClosing =>
+      _pos + 1 < content.length &&
+      (content.codeUnitAt(_pos) == $question ||
+          content.codeUnitAt(_pos) == $slash) &&
+      content.codeUnitAt(_pos + 1) == $gt;
+
+  bool get _isWhitespace {
+    var char = content.codeUnitAt(_pos);
+    return char == $space || char == $tab || char == $lf || char == $cr;
+  }
+
+  /// Parses an XML tag into a [ParseTagResult].
+  ParseTagResult parseXmlTag() {
+    // Walk until we find a tag.
+    while (_pos < content.length && content.codeUnitAt(_pos) != $lt) {
+      _pos++;
+    }
+    if (_pos >= content.length) {
+      return ParseTagResult.eof;
+    }
+    if (_isCommentOpening) {
+      return _parseComment();
+    }
+    if (_isDeclarationOpening) {
+      return _parseDeclaration();
+    }
+
+    return _parseNormalTag();
+  }
+
+  /// Returns whether [name] represents an element that is relevant to manifest
+  /// validation.
+  bool _isRelevantElement(String name) => _relevantElements.contains(name);
+
+  /// Parses any whitespace, returning `null` when non-whitespace is parsed.
+  ParseResult _parseAnyWhitespace() {
+    if (_pos >= content.length) {
+      return ParseResult.error;
+    }
+
+    while (_isWhitespace) {
+      _pos++;
+      if (_pos >= content.length) {
+        return ParseResult.error;
+      }
+    }
+    return null;
+  }
+
+  /// Parses an attribute.
+  ParseAttributeResult _parseAttribute(bool isRelevant) {
+    var attributes = <String, _XmlAttribute>{};
+    /*late*/ bool isEmptyElement;
+
+    while (true) {
+      if (_pos >= content.length) {
+        return ParseAttributeResult.error;
+      }
+      var char = content.codeUnitAt(_pos);
+
+      // In each loop, [_pos] must either be whitespace, ">", "/>", or "?>" to
+      // be valid.
+      if (_isClosing) {
+        isEmptyElement = false;
+        break;
+      } else if (_isTwoCharClosing) {
+        isEmptyElement = true;
+        _pos++;
+        break;
+      } else if (!_isWhitespace) {
+        return ParseAttributeResult.error;
+      }
+
+      var parsedWhitespaceResult = _parseAnyWhitespace();
+      if (parsedWhitespaceResult == ParseResult.error) {
+        return ParseAttributeResult.error;
+      }
+
+      if (_isClosing) {
+        isEmptyElement = false;
+        break;
+      } else if (_isTwoCharClosing) {
+        isEmptyElement = true;
+        _pos++;
+        break;
+      }
+
+      // Parse attribute name.
+      var attributeNamePos = _pos;
+      String attributeName;
+      _pos++;
+      if (_pos >= content.length) {
+        return ParseAttributeResult.error;
+      }
+
+      while ((char = content.codeUnitAt(_pos)) != $equal) {
+        if (_isWhitespace || _isClosing || _isTwoCharClosing) {
+          // An attribute without a value, while allowed in HTML, is not allowed
+          // in XML.
+          return ParseAttributeResult.error;
+        }
+        _pos++;
+        if (_pos >= content.length) {
+          return ParseAttributeResult.error;
+        }
+      }
+
+      if (isRelevant) {
+        attributeName = content.substring(attributeNamePos, _pos).toLowerCase();
+      }
+      _pos++; // Walk past "=".
+      if (_pos >= content.length) {
+        return ParseAttributeResult.error;
+      }
+
+      // Parse attribute value.
+      int quote;
+      char = content.codeUnitAt(_pos);
+      if (char == $apostrophe || char == $quote) {
+        quote = char;
+        _pos++;
+      } else {
+        // An attribute name, followed by "=", followed by ">" is an error.
+        return ParseAttributeResult.error;
+      }
+      int attributeValuePos = _pos;
+
+      while ((char = content.codeUnitAt(_pos)) != quote) {
+        _pos++;
+        if (_pos >= content.length) {
+          return ParseAttributeResult.error;
+        }
+      }
+
+      if (isRelevant) {
+        var attributeValue = content.substring(attributeValuePos, _pos);
+        var sourceSpan = sourceFile.span(attributeNamePos, _pos);
+        attributes[attributeName] =
+            _XmlAttribute(attributeName, attributeValue, sourceSpan);
+      }
+      _pos++;
+    }
+
+    var parseResult = isEmptyElement
+        ? ParseResult.attributesWithEmptyElementClose
+        : ParseResult.attributesWithTagClose;
+
+    return ParseAttributeResult(parseResult, attributes);
+  }
+
+  /// Parses a comment tag, as per https://www.w3.org/TR/xml/#sec-comments.
+  ParseTagResult _parseComment() {
+    // Walk past "<!--"
+    _pos += 4;
+    if (_pos >= content.length) {
+      return ParseTagResult.error;
+    }
+    while (!_isCommentClosing) {
+      _pos++;
+      if (_pos >= content.length) {
+        return ParseTagResult.error;
+      }
+    }
+    _pos += 2;
+
+    return ParseTagResult(ParseResult.element, null);
+  }
+
+  /// Parses a general declaration.
+  ///
+  /// Declarations are not processed or stored. The parser just intends to read
+  /// the tag and return.
+  ParseTagResult _parseDeclaration() {
+    // Walk past "<!"
+    _pos += 2;
+    if (_pos >= content.length) {
+      return ParseTagResult.error;
+    }
+    while (!_isClosing) {
+      _pos++;
+      if (_pos >= content.length) {
+        return ParseTagResult.error;
+      }
+    }
+
+    return ParseTagResult(ParseResult.element, null);
+  }
+
+  /// Parses a normal tag starting with an '<' character at the current
+  /// position.
+  ParseTagResult _parseNormalTag() {
+    var startPos = _pos;
+    _pos++;
+
+    if (_pos >= content.length) {
+      return ParseTagResult.error;
+    }
+
+    if (_isWhitespace) {
+      // A tag cannot begin with whitespace.
+      return ParseTagResult.error;
+    }
+
+    var isEndTag = content.codeUnitAt(_pos) == $slash;
+    if (isEndTag) _pos++;
+    var tagClosingState = _TagClosingState.notClosed;
+
+    // Parse name.
+    var namePos = _pos;
+    String name;
+
+    while (!_isClosing && !_isTwoCharClosing && !_isWhitespace) {
+      _pos++;
+      if (_pos >= content.length) {
+        return ParseTagResult.error;
+      }
+    }
+
+    if (_isClosing) {
+      // End of tag name, and tag.
+      name = content.substring(namePos, _pos).toLowerCase();
+      tagClosingState = _TagClosingState.closed;
+    } else if (_isTwoCharClosing) {
+      // End of tag name, tag, and element.
+      name = content.substring(namePos, _pos).toLowerCase();
+      tagClosingState = _TagClosingState.closedEmptyElement;
+      _pos++;
+    } else if (_isWhitespace) {
+      // End of tag name.
+      name = content.substring(namePos, _pos).toLowerCase();
+    }
+
+    if (isEndTag) {
+      var parsedWhitespaceResult = _parseAnyWhitespace();
+      if (parsedWhitespaceResult == ParseResult.error) {
+        return ParseTagResult.error;
+      }
+      if (_isClosing) {
+        // End tags cannot have attributes.
+        return ParseTagResult(
+            ParseResult.endTag, _XmlElement(name, {}, [], null));
+      } else {
+        return ParseTagResult.error;
+      }
+    }
+
+    var isRelevant = _isRelevantElement(name);
+
+    Map<String, _XmlAttribute> attributes;
+    bool isEmptyElement;
+    if (tagClosingState == _TagClosingState.notClosed) {
+      // Have not parsed the tag close yet; parse attributes.
+      var attributeResult = _parseAttribute(isRelevant);
+      var parseResult = attributeResult.parseResult;
+      if (parseResult == ParseResult.error) {
+        return ParseTagResult.error;
+      }
+      attributes = attributeResult.attributes;
+      isEmptyElement =
+          parseResult == ParseResult.attributesWithEmptyElementClose;
+    } else {
+      attributes = {};
+      isEmptyElement = tagClosingState == _TagClosingState.closedEmptyElement;
+    }
+    if (name.startsWith('!')) {
+      // Declarations (generally beginning with '!', do not require end tags.
+      isEmptyElement = true;
+    }
+
+    var children = <_XmlElement>[];
+    if (!isEmptyElement) {
+      ParseTagResult child;
+      _pos++;
+      // Parse any children, and end tag.
+      while ((child = parseXmlTag()).parseResult != ParseResult.endTag) {
+        if (child == ParseTagResult.eof || child == ParseTagResult.error) {
+          return child;
+        }
+        if (child.element == null) {
+          // Don't store an irrelevant element.
+          continue;
+        }
+        children.add(child.element);
+        _pos++;
+      }
+    }
+
+    // Finished parsing start tag.
+    if (isRelevant) {
+      var sourceSpan = sourceFile.span(startPos, _pos);
+      return ParseTagResult(ParseResult.relevantElement,
+          _XmlElement(name, attributes, children, sourceSpan));
+    } else {
+      // Discard all parsed children. This requires the notion that all relevant
+      // tags are direct children of other relevant tags.
+      return ParseTagResult(ParseResult.element, null);
+    }
+  }
+}
+
 class ManifestValidator {
   /// The source representing the file being validated.
   final Source source;
@@ -21,7 +392,11 @@
   ManifestValidator(this.source);
 
   /// Validate the [contents] of the Android Manifest file.
-  List<AnalysisError> validate(String contents, bool checkManifest) {
+  List<AnalysisError> validate(String content, bool checkManifest) {
+    // TODO(srawlins): Simplify [checkManifest] notion. Why call the method if
+    //  the caller always knows whether it should just return empty?
+    if (!checkManifest) return [];
+
     RecordingErrorListener recorder = RecordingErrorListener();
     ErrorReporter reporter = ErrorReporter(
       recorder,
@@ -29,109 +404,115 @@
       isNonNullableByDefault: false,
     );
 
-    if (checkManifest) {
-      var document =
-          parseFragment(contents, container: MANIFEST_TAG, generateSpans: true);
-      var manifest = document.children.firstWhere(
-          (element) => element.localName == MANIFEST_TAG,
-          orElse: () => null);
-      var features = manifest?.getElementsByTagName(USES_FEATURE_TAG) ?? [];
-      var permissions =
-          manifest?.getElementsByTagName(USES_PERMISSION_TAG) ?? [];
-      var activities = _findActivityElements(manifest);
+    var xmlParser = ManifestParser(content, source.uri);
 
-      _validateTouchScreenFeature(features, manifest, reporter);
-      _validateFeatures(features, reporter);
-      _validatePermissions(permissions, features, reporter);
-      _validateActivities(activities, reporter);
-    }
+    _checkManifestTag(xmlParser, reporter);
     return recorder.errors;
   }
 
-  List<Element> _findActivityElements(Element manifest) {
-    var applications = manifest?.getElementsByTagName(APPLICATION_TAG);
-    var applicationElement = (applications != null && applications.isNotEmpty)
-        ? applications.first
-        : null;
-    var activities =
-        applicationElement?.getElementsByTagName(ACTIVITY_TAG) ?? [];
-    return activities;
+  void _checkManifestTag(ManifestParser parser, ErrorReporter reporter) {
+    ParseTagResult parseTagResult;
+    while (
+        (parseTagResult = parser.parseXmlTag()).element?.name != MANIFEST_TAG) {
+      if (parseTagResult == ParseTagResult.eof ||
+          parseTagResult == ParseTagResult.error) {
+        return;
+      }
+    }
+
+    var manifestElement = parseTagResult.element;
+    var features =
+        manifestElement.children.where((e) => e.name == USES_FEATURE_TAG);
+    var permissions =
+        manifestElement.children.where((e) => e.name == USES_PERMISSION_TAG);
+    _validateTouchScreenFeature(features, manifestElement, reporter);
+    _validateFeatures(features, reporter);
+    _validatePermissions(permissions, features, reporter);
+
+    var application = manifestElement.children
+        .firstWhere((e) => e.name == APPLICATION_TAG, orElse: () => null);
+    if (application != null) {
+      for (var activity
+          in application.children.where((e) => e.name == ACTIVITY_TAG)) {
+        _validateActivity(activity, reporter);
+      }
+    }
   }
 
-  bool _hasFeatureCamera(List<Element> features) =>
-      features.any((f) => f.localName == HARDWARE_FEATURE_CAMERA);
+  bool _hasFeatureCamera(Iterable<_XmlElement> features) => features
+      .any((f) => f.attributes[ANDROID_NAME]?.value == HARDWARE_FEATURE_CAMERA);
 
-  bool _hasFeatureCameraAutoFocus(List<Element> features) =>
-      features.any((f) => f.localName == HARDWARE_FEATURE_CAMERA_AUTOFOCUS);
+  bool _hasFeatureCameraAutoFocus(Iterable<_XmlElement> features) =>
+      features.any((f) =>
+          f.attributes[ANDROID_NAME]?.value ==
+          HARDWARE_FEATURE_CAMERA_AUTOFOCUS);
 
   /// Report an error for the given node.
   void _reportErrorForNode(
-      ErrorReporter reporter, Node node, dynamic key, ErrorCode errorCode,
+      ErrorReporter reporter, _XmlElement node, String key, ErrorCode errorCode,
       [List<Object> arguments]) {
     FileSpan span =
-        key == null ? node.sourceSpan : node.attributeValueSpans[key];
+        key == null ? node.sourceSpan : node.attributes[key].sourceSpan;
     reporter.reportErrorForOffset(
         errorCode, span.start.offset, span.length, arguments);
   }
 
   /// Validate the 'activity' tags.
-  void _validateActivities(List<Element> activites, ErrorReporter reporter) {
-    activites.forEach((activity) {
-      var attributes = activity.attributes;
-      if (attributes.containsKey(ATTRIBUTE_SCREEN_ORIENTATION)) {
-        if (UNSUPPORTED_ORIENTATIONS
-            .contains(attributes[ATTRIBUTE_SCREEN_ORIENTATION])) {
-          _reportErrorForNode(reporter, activity, ATTRIBUTE_SCREEN_ORIENTATION,
-              ManifestWarningCode.SETTING_ORIENTATION_ON_ACTIVITY);
-        }
+  void _validateActivity(_XmlElement activity, ErrorReporter reporter) {
+    var attributes = activity.attributes;
+    if (attributes.containsKey(ATTRIBUTE_SCREEN_ORIENTATION)) {
+      if (UNSUPPORTED_ORIENTATIONS
+          .contains(attributes[ATTRIBUTE_SCREEN_ORIENTATION]?.value)) {
+        _reportErrorForNode(reporter, activity, ATTRIBUTE_SCREEN_ORIENTATION,
+            ManifestWarningCode.SETTING_ORIENTATION_ON_ACTIVITY);
       }
-      if (attributes.containsKey(ATTRIBUTE_RESIZEABLE_ACTIVITY)) {
-        if (attributes[ATTRIBUTE_RESIZEABLE_ACTIVITY] == 'false') {
-          _reportErrorForNode(reporter, activity, ATTRIBUTE_RESIZEABLE_ACTIVITY,
-              ManifestWarningCode.NON_RESIZABLE_ACTIVITY);
-        }
+    }
+    if (attributes.containsKey(ATTRIBUTE_RESIZEABLE_ACTIVITY)) {
+      if (attributes[ATTRIBUTE_RESIZEABLE_ACTIVITY]?.value == 'false') {
+        _reportErrorForNode(reporter, activity, ATTRIBUTE_RESIZEABLE_ACTIVITY,
+            ManifestWarningCode.NON_RESIZABLE_ACTIVITY);
       }
-    });
+    }
   }
 
   /// Validate the `uses-feature` tags.
-  void _validateFeatures(List<Element> features, ErrorReporter reporter) {
-    var unsupported = features
-        .where((element) => UNSUPPORTED_HARDWARE_FEATURES
-            .contains(element.attributes[ANDROID_NAME]))
-        .toList();
-    unsupported.forEach((element) {
+  void _validateFeatures(
+      Iterable<_XmlElement> features, ErrorReporter reporter) {
+    var unsupported = features.where((element) => UNSUPPORTED_HARDWARE_FEATURES
+        .contains(element.attributes[ANDROID_NAME]?.value));
+    for (var element in unsupported) {
       if (!element.attributes.containsKey(ANDROID_REQUIRED)) {
         _reportErrorForNode(
             reporter,
             element,
             ANDROID_NAME,
             ManifestWarningCode.UNSUPPORTED_CHROME_OS_HARDWARE,
-            [element.attributes[ANDROID_NAME]]);
-      } else if (element.attributes[ANDROID_REQUIRED] == 'true') {
+            [element.attributes[ANDROID_NAME]?.value]);
+      } else if (element.attributes[ANDROID_REQUIRED]?.value == 'true') {
         _reportErrorForNode(
             reporter,
             element,
             ANDROID_NAME,
             ManifestWarningCode.UNSUPPORTED_CHROME_OS_FEATURE,
-            [element.attributes[ANDROID_NAME]]);
+            [element.attributes[ANDROID_NAME]?.value]);
       }
-    });
+    }
   }
 
   /// Validate the `uses-permission` tags.
-  void _validatePermissions(List<Element> permissions, List<Element> features,
-      ErrorReporter reporter) {
-    permissions.forEach((permission) {
-      if (permission.attributes[ANDROID_NAME] == ANDROID_PERMISSION_CAMERA) {
+  void _validatePermissions(Iterable<_XmlElement> permissions,
+      Iterable<_XmlElement> features, ErrorReporter reporter) {
+    for (var permission in permissions) {
+      if (permission.attributes[ANDROID_NAME]?.value ==
+          ANDROID_PERMISSION_CAMERA) {
         if (!_hasFeatureCamera(features) ||
             !_hasFeatureCameraAutoFocus(features)) {
           _reportErrorForNode(reporter, permission, ANDROID_NAME,
               ManifestWarningCode.CAMERA_PERMISSIONS_INCOMPATIBLE);
         }
       } else {
-        var featureName =
-            getImpliedUnsupportedHardware(permission.attributes[ANDROID_NAME]);
+        var featureName = getImpliedUnsupportedHardware(
+            permission.attributes[ANDROID_NAME]?.value);
         if (featureName != null) {
           _reportErrorForNode(
               reporter,
@@ -141,15 +522,16 @@
               [featureName]);
         }
       }
-    });
+    }
   }
 
   /// Validate the presence/absence of the touchscreen feature tag.
-  void _validateTouchScreenFeature(
-      List<Element> features, Element manifest, ErrorReporter reporter) {
+  void _validateTouchScreenFeature(Iterable<_XmlElement> features,
+      _XmlElement manifest, ErrorReporter reporter) {
     var feature = features.firstWhere(
         (element) =>
-            element.attributes[ANDROID_NAME] == HARDWARE_FEATURE_TOUCHSCREEN,
+            element.attributes[ANDROID_NAME]?.value ==
+            HARDWARE_FEATURE_TOUCHSCREEN,
         orElse: () => null);
     if (feature != null) {
       if (!feature.attributes.containsKey(ANDROID_REQUIRED)) {
@@ -159,7 +541,7 @@
             ANDROID_NAME,
             ManifestWarningCode.UNSUPPORTED_CHROME_OS_HARDWARE,
             [HARDWARE_FEATURE_TOUCHSCREEN]);
-      } else if (feature.attributes[ANDROID_REQUIRED] == 'true') {
+      } else if (feature.attributes[ANDROID_REQUIRED]?.value == 'true') {
         _reportErrorForNode(
             reporter,
             feature,
@@ -173,3 +555,74 @@
     }
   }
 }
+
+@visibleForTesting
+class ParseAttributeResult {
+  static ParseAttributeResult error =
+      ParseAttributeResult(ParseResult.error, null);
+
+  final ParseResult parseResult;
+
+  final Map<String, _XmlAttribute> attributes;
+
+  ParseAttributeResult(this.parseResult, this.attributes);
+}
+
+enum ParseResult {
+  // Attributes were parsed, followed by a tag close like "/>", signifying an
+  // empty element, as per https://www.w3.org/TR/xml/#sec-starttags.
+  attributesWithEmptyElementClose,
+  // Attributes were parsed, followed by a tag close, ">".
+  attributesWithTagClose,
+  // A start tag for an irrelevant element was parsed, as per
+  // https://www.w3.org/TR/xml/#sec-starttags.
+  element,
+  // An end tag for an element was parsed, as per
+  // https://www.w3.org/TR/xml/#sec-starttags.
+  endTag,
+  // The content's EOF was parsed.
+  eof,
+  // An error was encountered.
+  error,
+  // A relevant element was parsed.
+  relevantElement,
+}
+
+@visibleForTesting
+class ParseTagResult {
+  static ParseTagResult eof = ParseTagResult(ParseResult.eof, null);
+  static ParseTagResult error = ParseTagResult(ParseResult.error, null);
+
+  final ParseResult parseResult;
+
+  final _XmlElement element;
+
+  ParseTagResult(this.parseResult, this.element);
+}
+
+enum _TagClosingState {
+  // Represents that the tag's close has not been parsed.
+  notClosed,
+  // Represents that the tag's close has been parsed as ">".
+  closed,
+  // Represents that the tag's close has been parsed as "/>", "?>", indicating
+  // an empty element, as per https://www.w3.org/TR/xml/#sec-starttags.
+  closedEmptyElement,
+}
+
+class _XmlAttribute {
+  final String name;
+  final String value;
+  final SourceSpan sourceSpan;
+
+  _XmlAttribute(this.name, this.value, this.sourceSpan);
+}
+
+class _XmlElement {
+  final String name;
+  final Map<String, _XmlAttribute> attributes;
+  final List<_XmlElement> children;
+  final SourceSpan sourceSpan;
+
+  _XmlElement(this.name, this.attributes, this.children, this.sourceSpan);
+}
diff --git a/pkg/analyzer/lib/src/manifest/manifest_values.dart b/pkg/analyzer/lib/src/manifest/manifest_values.dart
index ddc3bfa..f3f8b7a 100644
--- a/pkg/analyzer/lib/src/manifest/manifest_values.dart
+++ b/pkg/analyzer/lib/src/manifest/manifest_values.dart
@@ -17,15 +17,18 @@
 
 const String APPLICATION_TAG = 'application';
 
+/// The Android resizeableActivity attribute.
+// The parser does not maintain camelcase for attributes. Uses
+// 'resizeableactivity' instead of 'resizeableActivity'
 const String ATTRIBUTE_RESIZEABLE_ACTIVITY = 'android:resizeableactivity';
 
+/// The Android screenOrientation attribute.
+// The parser does not maintain camelcase for attributes. Uses
+// 'screenorientation' instead of 'screenOrientation'.
 const String ATTRIBUTE_SCREEN_ORIENTATION = 'android:screenorientation';
 
-// The parser does not maintain camelcase for attributes
-// Use 'resizeableactivity' instead of 'resizeableActivity'
 const String HARDWARE_FEATURE_CAMERA = 'android.hardware.camera';
 
-// Use 'screenorientation' instead of 'screenOrientation'
 const String HARDWARE_FEATURE_CAMERA_AUTOFOCUS =
     'android.hardware.camera.autofocus';
 
diff --git a/pkg/analyzer/lib/src/workspace/basic.dart b/pkg/analyzer/lib/src/workspace/basic.dart
index 8b3e3c8..fb02a62 100644
--- a/pkg/analyzer/lib/src/workspace/basic.dart
+++ b/pkg/analyzer/lib/src/workspace/basic.dart
@@ -76,4 +76,8 @@
     // is in the package as well.
     return workspace.provider.pathContext.isWithin(root, filePath);
   }
+
+  @override
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath) =>
+      workspace.packageMap;
 }
diff --git a/pkg/analyzer/lib/src/workspace/bazel.dart b/pkg/analyzer/lib/src/workspace/bazel.dart
index 9e60a4f..615fb32 100644
--- a/pkg/analyzer/lib/src/workspace/bazel.dart
+++ b/pkg/analyzer/lib/src/workspace/bazel.dart
@@ -538,4 +538,10 @@
     // learning exactly which package [filePath] is contained in.
     return workspace.findPackageFor(filePath).root == root;
   }
+
+  @override
+  // TODO(brianwilkerson) Implement this by looking in the BUILD file for 'deps'
+  //  lists.
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath) =>
+      <String, List<Folder>>{};
 }
diff --git a/pkg/analyzer/lib/src/workspace/gn.dart b/pkg/analyzer/lib/src/workspace/gn.dart
index 235418e..cca441f 100644
--- a/pkg/analyzer/lib/src/workspace/gn.dart
+++ b/pkg/analyzer/lib/src/workspace/gn.dart
@@ -233,4 +233,8 @@
     // learning exactly which package [filePath] is contained in.
     return workspace.findPackageFor(filePath).root == root;
   }
+
+  @override
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath) =>
+      workspace.packageMap;
 }
diff --git a/pkg/analyzer/lib/src/workspace/package_build.dart b/pkg/analyzer/lib/src/workspace/package_build.dart
index 39fdaaf..60eb742 100644
--- a/pkg/analyzer/lib/src/workspace/package_build.dart
+++ b/pkg/analyzer/lib/src/workspace/package_build.dart
@@ -330,4 +330,8 @@
 
     return false;
   }
+
+  @override
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath) =>
+      workspace._packageMap;
 }
diff --git a/pkg/analyzer/lib/src/workspace/pub.dart b/pkg/analyzer/lib/src/workspace/pub.dart
index c70198c..f47327d 100644
--- a/pkg/analyzer/lib/src/workspace/pub.dart
+++ b/pkg/analyzer/lib/src/workspace/pub.dart
@@ -113,4 +113,11 @@
     // is in the package as well.
     return workspace.provider.pathContext.isWithin(root, filePath);
   }
+
+  @override
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath) {
+    // TODO(brianwilkerson) Consider differentiating based on whether the
+    //  [libraryPath] is inside the `lib` directory.
+    return workspace.packageMap;
+  }
 }
diff --git a/pkg/analyzer/lib/src/workspace/workspace.dart b/pkg/analyzer/lib/src/workspace/workspace.dart
index e37fd1a..d9dc17a 100644
--- a/pkg/analyzer/lib/src/workspace/workspace.dart
+++ b/pkg/analyzer/lib/src/workspace/workspace.dart
@@ -2,6 +2,7 @@
 // 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/file_system/file_system.dart';
 import 'package:analyzer/src/generated/sdk.dart';
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/summary/package_bundle_reader.dart';
@@ -61,6 +62,11 @@
       return source.fullName;
     }
   }
+
+  /// Return a map from the names of packages to the absolute and normalized
+  /// path of the root of those packages for all of the packages that could
+  /// validly be imported by the library with the given [libraryPath].
+  Map<String, List<Folder>> packagesAvailableTo(String libraryPath);
 }
 
 /// An interface for a workspace that contains a default analysis options file.
diff --git a/pkg/analyzer/pubspec.yaml b/pkg/analyzer/pubspec.yaml
index ce9a46b..2e7094e 100644
--- a/pkg/analyzer/pubspec.yaml
+++ b/pkg/analyzer/pubspec.yaml
@@ -9,12 +9,12 @@
 dependencies:
   _fe_analyzer_shared: ^8.0.0
   args: ^1.0.0
+  charcode: ^1.1.2
   cli_util: '>=0.1.4 <0.3.0'
   collection: ^1.10.1
   convert: ^2.0.0
   crypto: ^2.0.0
   glob: ^1.0.3
-  html: '>=0.13.4+1 <0.15.0'
   meta: ^1.0.2
   package_config: ^1.0.0
   path: ^1.0.0
diff --git a/pkg/analyzer/test/src/manifest/manifest_validator_test.dart b/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
index 717cf1f..4f2dce2 100644
--- a/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
+++ b/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
@@ -6,8 +6,10 @@
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/manifest/manifest_validator.dart';
+import 'package:analyzer/src/manifest/manifest_values.dart';
 import 'package:analyzer/src/manifest/manifest_warning_code.dart';
 import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../../generated/test_support.dart';
@@ -15,10 +17,409 @@
 main() {
   defineReflectiveSuite(() {
     defineReflectiveTests(ManifestValidatorTest);
+    defineReflectiveTests(ManifestParserTest);
   });
 }
 
 @reflectiveTest
+class ManifestParserTest with ResourceProviderMixin {
+  static final _manifestUri = Uri.parse('file:///sample/Manifest.xml');
+
+  void test_attribute_endsAfterEquals_isError() {
+    var parser = ManifestParser('<tag a= />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_attribute_missingValue_isError() {
+    var parser = ManifestParser('<tag a />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_attribute_valueMissingQuotes_isError() {
+    var parser = ManifestParser('<tag a=b />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_commentTag_isParsed() {
+    var parser = ManifestParser('''
+<!-- comment tag -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.element);
+    expect(result.element, isNull);
+
+    result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_emptyFileDoesNotCrash() {
+    var parser = ManifestParser('', _manifestUri);
+    parser.parseXmlTag();
+  }
+
+  void test_endTagWithAttributes_isError() {
+    var parser = ManifestParser('<tag></tag aaa="bbb">', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_endTagWithWhitespace_isOk() {
+    var parser = ManifestParser('<tag></tag >', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.element);
+  }
+
+  void test_eofAfterAttributeEqual_isError() {
+    var parser = ManifestParser('<manifest xml=', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofAfterAttributeEqual_whitespace_isError() {
+    var parser = ManifestParser('<manifest xml= ', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofAfterOpeningTag() {
+    var parser = ManifestParser('<manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.eof);
+  }
+
+  void test_eofAfterOpeningTag_nested_inside() {
+    var parser = ManifestParser('<manifest><application>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.eof);
+  }
+
+  void test_eofAfterOpeningTag_nested_outside() {
+    var parser =
+        ManifestParser('<manifest><application></application>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.eof);
+  }
+
+  void test_eofAfterOpeningTag_whitespace() {
+    var parser = ManifestParser('<tag> ', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.eof);
+  }
+
+  void test_eofDuringAttributeName_isError() {
+    var parser = ManifestParser('<tag xml ', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofDuringAttributeName_whitespace_isError() {
+    var parser = ManifestParser('<tag xml', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofDuringAttributeValue_isError() {
+    var parser = ManifestParser('<tag a="b"', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofDuringAttributeValue_whitespace_isError() {
+    var parser = ManifestParser('<tag aaa="bbb" ', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofDuringTagName_isError() {
+    var parser = ManifestParser('<tag', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_eofDuringTagName_whitespace_isError() {
+    var parser = ManifestParser('<tag ', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+
+  void test_manifestTag_attributeWithEmptyValue_emptyElement_isParsed() {
+    var parser = ManifestParser('<manifest xmlns:android=""/>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_emptyElement_isParsed() {
+    var parser = ManifestParser('''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_emptyElement_noAttributes_isParsed() {
+    var parser = ManifestParser('<manifest/>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_emptyElement_noAttributes_whitespace_isParsed() {
+    var parser = ManifestParser('<manifest />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_emptyElement_whitespace_isParsed() {
+    var parser = ManifestParser('''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_isParsed() {
+    var parser = ManifestParser('''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_uppercase_isParsed() {
+    var parser = ManifestParser('''
+<MANIFEST xmlns:android="http://schemas.android.com/apk/res/android">
+</MANIFEST>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_withDoctype_isParsed() {
+    var parser = ManifestParser('''
+<!DOCTYPE greeting SYSTEM "hello.dtd">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.element);
+    expect(result.element, isNull);
+
+    result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_withFeatures_isParsed() {
+    var parser = ManifestParser('''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses-feature android:name="android.hardware.touchscreen"
+      android:required="false" />
+  <uses-feature android:name="android.software.home_screen" />
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    var children = result.element.children;
+    expect(children, hasLength(2));
+
+    expect(children[0].name, equals(USES_FEATURE_TAG));
+    var touchscreenAttributes = children[0].attributes;
+    expect(touchscreenAttributes, hasLength(2));
+    expect(touchscreenAttributes[ANDROID_NAME].value,
+        equals(HARDWARE_FEATURE_TOUCHSCREEN));
+    expect(touchscreenAttributes[ANDROID_REQUIRED].value, equals('false'));
+
+    expect(children[1].name, equals(USES_FEATURE_TAG));
+    var homeScreenAttributes = children[1].attributes;
+    expect(homeScreenAttributes, hasLength(1));
+    expect(homeScreenAttributes[ANDROID_NAME].value,
+        equals('android.software.home_screen'));
+  }
+
+  void test_manifestTag_withInnerText_isParsed() {
+    var parser = ManifestParser('''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+Text
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_withSurroundingText_isParsed() {
+    var parser = ManifestParser('''
+Text
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
+Text
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_manifestTag_withXmlTag_isParsed() {
+    var parser = ManifestParser('''
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
+''', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.element);
+    expect(result.element, isNull);
+
+    result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.relevantElement);
+    expect(result.element.name, MANIFEST_TAG);
+  }
+
+  void test_outsideTagClosedBeforeInside() {
+    var parser =
+        ManifestParser('<manifest><application></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.eof);
+  }
+
+  void test_relevantTag_attributeIsParsed() {
+    var parser =
+        ManifestParser('<manifest aaa="bbb"></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['aaa'];
+    expect(attribute, isNotNull);
+    expect(attribute.name, equals('aaa'));
+    expect(attribute.value, equals('bbb'));
+    var sourceSpan = attribute.sourceSpan;
+    expect(sourceSpan.start.offset, equals(10));
+    expect(sourceSpan.end.offset, equals(18));
+  }
+
+  void test_relevantTag_attributeIsParsed_containsSingleQuotes() {
+    var parser =
+        ManifestParser('<manifest aaa="b\'b\'b"></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['aaa'];
+    expect(attribute, isNotNull);
+    expect(attribute.name, equals('aaa'));
+    expect(attribute.value, equals("b'b'b"));
+    var sourceSpan = attribute.sourceSpan;
+    expect(sourceSpan.start.offset, equals(10));
+    expect(sourceSpan.end.offset, equals(20));
+  }
+
+  void test_relevantTag_attributeIsParsed_emptyValue() {
+    var parser = ManifestParser('<manifest aaa=""></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['aaa'];
+    expect(attribute, isNotNull);
+    expect(attribute.name, equals('aaa'));
+    expect(attribute.value, equals(''));
+    var sourceSpan = attribute.sourceSpan;
+    expect(sourceSpan.start.offset, equals(10));
+    expect(sourceSpan.end.offset, equals(15));
+  }
+
+  void test_relevantTag_attributeIsParsed_singleQuotes() {
+    var parser =
+        ManifestParser("<manifest aaa='bbb'></manifest>", _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['aaa'];
+    expect(attribute, isNotNull);
+    expect(attribute.name, equals('aaa'));
+    expect(attribute.value, equals('bbb'));
+    var sourceSpan = attribute.sourceSpan;
+    expect(sourceSpan.start.offset, equals(10));
+    expect(sourceSpan.end.offset, equals(18));
+  }
+
+  void test_relevantTag_attributeIsParsed_uppercase() {
+    var parser =
+        ManifestParser('<manifest AAA="bbb"></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['aaa'];
+    expect(attribute, isNotNull);
+    expect(attribute.name, equals('aaa'));
+    expect(attribute.value, equals('bbb'));
+  }
+
+  void test_relevantTag_attributeWithEmptyValueIsParsed() {
+    var parser =
+        ManifestParser('<manifest xmlns:android=""></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    expect(result.element.attributes, hasLength(1));
+    var attribute = result.element.attributes['xmlns:android'];
+    expect(attribute, isNotNull);
+    expect(attribute.value, equals(''));
+  }
+
+  void test_relevantTag_emptyElement_nameIsParsed() {
+    var parser = ManifestParser('<manifest/>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    var sourceSpan = result.element.sourceSpan;
+    expect(sourceSpan.start.offset, equals(0));
+    expect(sourceSpan.end.offset, equals(10));
+  }
+
+  void test_relevantTag_emptyElement_whitespace_nameIsParsed() {
+    var parser = ManifestParser('<manifest />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    var sourceSpan = result.element.sourceSpan;
+    expect(sourceSpan.start.offset, equals(0));
+    expect(sourceSpan.end.offset, equals(11));
+  }
+
+  void test_relevantTag_withAttributes_emptyElement_nameIsParsed() {
+    var parser = ManifestParser('<manifest aaa="bbb" />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    var sourceSpan = result.element.sourceSpan;
+    expect(sourceSpan.start.offset, equals(0));
+    expect(sourceSpan.end.offset, equals(21));
+  }
+
+  void test_relevantTag_withAttributes_nameIsParsed() {
+    var parser =
+        ManifestParser('<manifest aaa="bbb"></manifest>', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.element.name, MANIFEST_TAG);
+    var sourceSpan = result.element.sourceSpan;
+    expect(sourceSpan.start.offset, equals(0));
+    expect(sourceSpan.end.offset, equals(30));
+  }
+
+  void test_tagBeginningWithWhitespace_isError() {
+    var parser = ManifestParser('< tag />', _manifestUri);
+    var result = parser.parseXmlTag();
+    expect(result.parseResult, ParseResult.error);
+  }
+}
+
+@reflectiveTest
 class ManifestValidatorTest with ResourceProviderMixin {
   ManifestValidator validator;
 
@@ -53,6 +454,18 @@
 ''', [ManifestWarningCode.CAMERA_PERMISSIONS_INCOMPATIBLE]);
   }
 
+  test_cameraPermissions_ok() {
+    assertNoErrors('''
+<manifest
+     xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-feature android:name="android.hardware.camera" android:required="false" />
+    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
+    <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
+    <uses-permission android:name="android.permission.CAMERA" />
+</manifest>
+''');
+  }
+
   test_featureNotSupported_error() {
     assertErrors('''
 <manifest
diff --git a/pkg/analyzer/test/src/workspace/basic_test.dart b/pkg/analyzer/test/src/workspace/basic_test.dart
index f338eba..2ed06b8 100644
--- a/pkg/analyzer/test/src/workspace/basic_test.dart
+++ b/pkg/analyzer/test/src/workspace/basic_test.dart
@@ -8,6 +8,7 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../../generated/test_support.dart';
+import 'workspace_test_support.dart';
 
 main() {
   defineReflectiveSuite(() {
@@ -17,21 +18,23 @@
 }
 
 @reflectiveTest
-class BasicWorkspacePackageTest with ResourceProviderMixin {
-  BasicWorkspace workspace;
-
+class BasicWorkspacePackageTest extends WorkspacePackageTest {
   setUp() {
     newFolder('/workspace');
-    workspace =
-        BasicWorkspace.find(resourceProvider, {}, convertPath('/workspace'));
+    workspace = BasicWorkspace.find(
+        resourceProvider,
+        {
+          'p1': [getFolder('/.pubcache/p1/lib')],
+          'workspace': [getFolder('/workspace/lib')]
+        },
+        convertPath('/workspace'));
     expect(workspace.isBazel, isFalse);
   }
 
   void test_contains_differentWorkspace() {
     newFile('/workspace2/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/code.dart'));
+    var package = findPackage('/workspace/project/lib/code.dart');
     expect(
         package.contains(
             TestSource(convertPath('/workspace2/project/lib/file.dart'))),
@@ -41,8 +44,7 @@
   void test_contains_sameWorkspace() {
     newFile('/workspace/project/lib/file2.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/code.dart'));
+    var package = findPackage('/workspace/project/lib/code.dart');
     expect(
         package.contains(
             TestSource(convertPath('/workspace/project/lib/file2.dart'))),
@@ -60,8 +62,7 @@
   void test_findPackageFor_includedFile() {
     newFile('/workspace/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/file.dart'));
+    var package = findPackage('/workspace/project/lib/file.dart');
     expect(package, isNotNull);
     expect(package.root, convertPath('/workspace'));
     expect(package.workspace, equals(workspace));
@@ -70,10 +71,16 @@
   void test_findPackageFor_unrelatedFile() {
     newFile('/workspace/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace2/project/lib/file.dart'));
+    var package = findPackage('/workspace2/project/lib/file.dart');
     expect(package, isNull);
   }
+
+  void test_packagesAvailableTo() {
+    var libraryPath = convertPath('/workspace/lib/test.dart');
+    var package = findPackage(libraryPath);
+    var packageMap = package.packagesAvailableTo(libraryPath);
+    expect(packageMap.keys, unorderedEquals(['p1', 'workspace']));
+  }
 }
 
 @reflectiveTest
diff --git a/pkg/analyzer/test/src/workspace/bazel_test.dart b/pkg/analyzer/test/src/workspace/bazel_test.dart
index 6bc5fb9..868084b 100644
--- a/pkg/analyzer/test/src/workspace/bazel_test.dart
+++ b/pkg/analyzer/test/src/workspace/bazel_test.dart
@@ -696,6 +696,13 @@
     expect(package.workspace, equals(workspace));
   }
 
+  void test_packagesAvailableTo() {
+    _setUpPackage();
+    var packageMap =
+        package.packagesAvailableTo(convertPath('/ws/some/code/lib/code.dart'));
+    expect(packageMap, isEmpty);
+  }
+
   /// Create new files and directories from [paths].
   void _addResources(List<String> paths) {
     for (String path in paths) {
diff --git a/pkg/analyzer/test/src/workspace/gn_test.dart b/pkg/analyzer/test/src/workspace/gn_test.dart
index 0ed21d5..92244fb 100644
--- a/pkg/analyzer/test/src/workspace/gn_test.dart
+++ b/pkg/analyzer/test/src/workspace/gn_test.dart
@@ -93,11 +93,23 @@
     expect(package, isNull);
   }
 
+  void test_packagesAvailableTo() {
+    GnWorkspace workspace = _buildStandardGnWorkspace();
+    newFile('/ws/some/code/BUILD.gn');
+    var libraryPath = newFile('/ws/some/code/lib/code.dart').path;
+    var package = workspace.findPackageFor(libraryPath);
+    var packageMap = package.packagesAvailableTo(libraryPath);
+    expect(packageMap.keys, unorderedEquals(['p1', 'workspace']));
+  }
+
   GnWorkspace _buildStandardGnWorkspace() {
     newFolder('/ws/.jiri_root');
     String buildDir = convertPath('out/debug-x87_128');
     newFile('/ws/.fx-build-dir', content: '$buildDir\n');
-    newFile('/ws/out/debug-x87_128/dartlang/gen/some/code/foo.packages');
+    newFile('/ws/out/debug-x87_128/dartlang/gen/some/code/foo.packages',
+        content: '''
+p1:file:///some/path/lib/
+workspace:lib/''');
     newFolder('/ws/some/code');
     var gnWorkspace =
         GnWorkspace.find(resourceProvider, convertPath('/ws/some/code'));
diff --git a/pkg/analyzer/test/src/workspace/pub_test.dart b/pkg/analyzer/test/src/workspace/pub_test.dart
index a33908a..399300f 100644
--- a/pkg/analyzer/test/src/workspace/pub_test.dart
+++ b/pkg/analyzer/test/src/workspace/pub_test.dart
@@ -8,6 +8,7 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../../generated/test_support.dart';
+import 'workspace_test_support.dart';
 
 main() {
   defineReflectiveSuite(() {
@@ -17,21 +18,23 @@
 }
 
 @reflectiveTest
-class PubWorkspacePackageTest with ResourceProviderMixin {
-  PubWorkspace workspace;
-
+class PubWorkspacePackageTest extends WorkspacePackageTest {
   setUp() {
     newFile('/workspace/pubspec.yaml', content: 'name: project');
-    workspace =
-        PubWorkspace.find(resourceProvider, {}, convertPath('/workspace'));
+    workspace = PubWorkspace.find(
+        resourceProvider,
+        {
+          'p1': [getFolder('/.pubcache/p1/lib')],
+          'workspace': [getFolder('/workspace/lib')]
+        },
+        convertPath('/workspace'));
     expect(workspace.isBazel, isFalse);
   }
 
   void test_contains_differentWorkspace() {
     newFile('/workspace2/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/code.dart'));
+    var package = findPackage('/workspace/project/lib/code.dart');
     expect(
         package.contains(
             TestSource(convertPath('/workspace2/project/lib/file.dart'))),
@@ -41,8 +44,7 @@
   void test_contains_sameWorkspace() {
     newFile('/workspace/project/lib/file2.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/code.dart'));
+    var package = findPackage('/workspace/project/lib/code.dart');
     expect(
         package.contains(
             TestSource(convertPath('/workspace/project/lib/file2.dart'))),
@@ -60,8 +62,7 @@
   void test_findPackageFor_includedFile() {
     newFile('/workspace/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace/project/lib/file.dart'));
+    var package = findPackage('/workspace/project/lib/file.dart');
     expect(package, isNotNull);
     expect(package.root, convertPath('/workspace'));
     expect(package.workspace, equals(workspace));
@@ -70,10 +71,16 @@
   void test_findPackageFor_unrelatedFile() {
     newFile('/workspace/project/lib/file.dart');
 
-    var package = workspace
-        .findPackageFor(convertPath('/workspace2/project/lib/file.dart'));
+    var package = findPackage('/workspace2/project/lib/file.dart');
     expect(package, isNull);
   }
+
+  void test_packagesAvailableTo() {
+    var libraryPath = convertPath('/workspace/lib/test.dart');
+    var package = findPackage(libraryPath);
+    var packageMap = package.packagesAvailableTo(libraryPath);
+    expect(packageMap.keys, unorderedEquals(['p1', 'workspace']));
+  }
 }
 
 @reflectiveTest
diff --git a/pkg/analyzer/test/src/workspace/workspace_test_support.dart b/pkg/analyzer/test/src/workspace/workspace_test_support.dart
new file mode 100644
index 0000000..164e1e4
--- /dev/null
+++ b/pkg/analyzer/test/src/workspace/workspace_test_support.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, 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/test_utilities/resource_provider_mixin.dart';
+import 'package:analyzer/src/workspace/workspace.dart';
+
+/// Utilities for tests of subclasses of [WorkspacePackage].
+abstract class WorkspacePackageTest with ResourceProviderMixin {
+  /// The workspace containing the packages.
+  Workspace workspace;
+
+  /// Return the package containing the given [path], or `null` if there is no
+  /// such package in the [workspace].
+  WorkspacePackage findPackage(String path) =>
+      workspace.findPackageFor(convertPath(path));
+}
diff --git a/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart b/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
index a7304ff..96eb5df 100644
--- a/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
+++ b/pkg/compiler/lib/src/inferrer/powersets/powerset_bits.dart
@@ -6,7 +6,7 @@
 import '../../constants/values.dart';
 import '../../elements/entities.dart';
 import '../../elements/names.dart';
-import '../../elements/types.dart' show DartType, InterfaceType;
+import '../../elements/types.dart';
 import '../../ir/static_type.dart';
 import '../../universe/selector.dart';
 import '../../world.dart';
@@ -49,6 +49,8 @@
 
   CommonElements get commonElements => _closedWorld.commonElements;
 
+  DartTypes get dartTypes => _closedWorld.dartTypes;
+
   int get trueMask => 1 << _trueIndex;
   int get falseMask => 1 << _falseIndex;
   int get nullMask => 1 << _nullIndex;
@@ -376,17 +378,131 @@
 
   int createFromStaticType(DartType type,
       {ClassRelation classRelation = ClassRelation.subtype, bool nullable}) {
-    // TODO(coam): This only works for bool
-    int bits = otherMask;
-    if (type is InterfaceType && _isBoolSubtype(type.element)) {
-      bits = boolMask;
+    assert(nullable != null);
+
+    if ((classRelation == ClassRelation.subtype ||
+            classRelation == ClassRelation.thisExpression) &&
+        dartTypes.isTopType(type)) {
+      // A cone of a top type includes all values. This would be 'precise' if we
+      // tracked that.
+      return dynamicType;
     }
-    if (nullable) {
-      bits = bits | nullMask;
+
+    if (type is NullableType) {
+      assert(dartTypes.useNullSafety);
+      return _createFromStaticType(type.baseType, classRelation, true);
     }
-    return bits;
+
+    if (type is LegacyType) {
+      assert(dartTypes.useNullSafety);
+      DartType baseType = type.baseType;
+      if (baseType is NeverType) {
+        // Never* is same as Null, for both 'is' and 'as'.
+        return nullMask;
+      }
+
+      // Object* is a top type for both 'is' and 'as'. This is handled in the
+      // 'cone of top type' case above.
+
+      return _createFromStaticType(baseType, classRelation, nullable);
+    }
+
+    if (dartTypes.useLegacySubtyping) {
+      // In legacy and weak mode, `String` is nullable depending on context.
+      return _createFromStaticType(type, classRelation, nullable);
+    } else {
+      // In strong mode nullability comes from explicit NullableType.
+      return _createFromStaticType(type, classRelation, false);
+    }
   }
 
+  int _createFromStaticType(
+      DartType type, ClassRelation classRelation, bool nullable) {
+    assert(nullable != null);
+
+    int finish(int value, bool isPrecise) {
+      // [isPrecise] is ignored since we only treat singleton partitions as
+      // precise.
+      // TODO(sra): Each bit that represents more that one concrete value could
+      // have an 'isPrecise' bit.
+      return nullable ? includeNull(value) : value;
+    }
+
+    bool isPrecise = true;
+    while (type is TypeVariableType) {
+      TypeVariableType typeVariable = type;
+      type = _closedWorld.elementEnvironment
+          .getTypeVariableBound(typeVariable.element);
+      classRelation = ClassRelation.subtype;
+      isPrecise = false;
+      if (type is NullableType) {
+        // <A extends B?, B extends num>  ...  null is A --> can be `true`.
+        // <A extends B, B extends num?>  ...  null is A --> can be `true`.
+        nullable = true;
+        type = type.withoutNullability;
+      }
+    }
+
+    if ((classRelation == ClassRelation.thisExpression ||
+            classRelation == ClassRelation.subtype) &&
+        dartTypes.isTopType(type)) {
+      // A cone of a top type includes all values. Since we already tested this
+      // in [createFromStaticType], we get here only for type parameter bounds.
+      return finish(dynamicType, isPrecise);
+    }
+
+    if (type is InterfaceType) {
+      ClassEntity cls = type.element;
+      List<DartType> arguments = type.typeArguments;
+      if (isPrecise && arguments.isNotEmpty) {
+        // Can we ignore the type arguments?
+        //
+        // For legacy covariance, if the interface type is a generic interface
+        // type and is maximal (i.e. instantiated to bounds), the typemask,
+        // which is based on the class element, is still precise. We check
+        // against Top for the parameter arguments since we don't have a
+        // convenient check for instantation to bounds.
+        //
+        // TODO(sra): Check arguments against bounds.
+        // TODO(sra): Handle other variances.
+        List<Variance> variances = dartTypes.getTypeVariableVariances(cls);
+        for (int i = 0; i < arguments.length; i++) {
+          Variance variance = variances[i];
+          DartType argument = arguments[i];
+          if (variance == Variance.legacyCovariant &&
+              dartTypes.isTopType(argument)) {
+            continue;
+          }
+          isPrecise = false;
+        }
+      }
+      switch (classRelation) {
+        case ClassRelation.exact:
+          return finish(createNonNullExact(cls), isPrecise);
+        case ClassRelation.thisExpression:
+          if (!_closedWorld.isUsedAsMixin(cls)) {
+            return finish(createNonNullSubclass(cls), isPrecise);
+          }
+          break;
+        case ClassRelation.subtype:
+          break;
+      }
+      return finish(createNonNullSubtype(cls), isPrecise);
+    }
+
+    if (type is FunctionType) {
+      return finish(createNonNullSubtype(commonElements.functionClass), false);
+    }
+
+    if (type is NeverType) {
+      return finish(emptyType, isPrecise);
+    }
+
+    return finish(dynamicType, false);
+  }
+
+  int get dynamicType => powersetTop;
+
   int get asyncStarStreamType => powersetTop;
 
   int get asyncFutureType => powersetTop;
diff --git a/pkg/compiler/test/deferred_loading/data/regress_35311/lib.dart b/pkg/compiler/test/deferred_loading/data/regress_35311/lib.dart
new file mode 100644
index 0000000..ee3d8b3
--- /dev/null
+++ b/pkg/compiler/test/deferred_loading/data/regress_35311/lib.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, 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.
+
+/*class: B:OutputUnit(1, {lib}), type=OutputUnit(main, {})*/
+/*member: B.:OutputUnit(1, {lib})*/
+class B {
+  /*member: B.value:OutputUnit(1, {lib})*/
+  B value = null;
+}
+
+/*member: list:OutputUnit(1, {lib})*/
+List<B> list = [];
diff --git a/pkg/compiler/test/deferred_loading/data/regress_35311/main.dart b/pkg/compiler/test/deferred_loading/data/regress_35311/main.dart
new file mode 100644
index 0000000..7b8802a
--- /dev/null
+++ b/pkg/compiler/test/deferred_loading/data/regress_35311/main.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2020, 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 'lib.dart' deferred as lib;
+
+/*member: main:OutputUnit(main, {})*/
+main() async {
+  await lib.loadLibrary();
+
+  // inferred return-type in closures:
+  // lib.B f1() => lib.B(); // Compile time error(see tests/dart2js)
+  var f2 = /*OutputUnit(main, {})*/ () =>
+      lib.B(); // no compile error, but f1 has inferred type: () -> d.B
+
+  // inferred type-arguments
+  // lib.list = <lib.B>[]; // Compile time error(see tests/dart2js)
+  lib.list = []; // no error, but type parameter was injected here
+  lib.list = lib.list
+      .map(/*OutputUnit(main, {})*/ (x) => x.value)
+      .toList(); // no Compile error, type parameter inferred on closure and map<T>.
+}
diff --git a/tests/dart2js/deferred/regress_35311/lib.dart b/tests/dart2js/deferred/regress_35311/lib.dart
new file mode 100644
index 0000000..605c42c
--- /dev/null
+++ b/tests/dart2js/deferred/regress_35311/lib.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2020, 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.
+
+class B {
+  B? value = null;
+}
+
+List<B> list = [];
diff --git a/tests/dart2js/deferred/regress_35311/regress_35311_test.dart b/tests/dart2js/deferred/regress_35311/regress_35311_test.dart
new file mode 100644
index 0000000..7779291
--- /dev/null
+++ b/tests/dart2js/deferred/regress_35311/regress_35311_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2020, 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 'lib.dart' deferred as lib;
+
+main() async {
+  await lib.loadLibrary();
+
+  // inferred return-type in closures:
+  lib.B f1() => lib.B(); //# 01: compile-time error
+  var f2 = () => lib.B(); // no error, but f1 has inferred type: () -> d.B
+
+  // inferred type-arguments
+  lib.list = <lib.B>[]; //# 02: compile-time error
+  lib.list = []; // no error, but type parameter was injected here
+  lib.list = lib.list.map((x) => x.value!).toList(); // no error, type parameter inferred on closure and map<T>.
+}
diff --git a/tools/VERSION b/tools/VERSION
index 59e1d50..ce0ea53 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 10
 PATCH 0
-PRERELEASE 70
+PRERELEASE 71
 PRERELEASE_PATCH 0
\ No newline at end of file