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