Support `<ng-container>` properly. (#628)
* Support `<ng-container>` properly.
Also refactored `converter.dart` to share opening/close (name)span logic
across the various special tag types (`<normal>`, `<template`,
`<ng-content>`, `<ng-container>`.
Add an assert that all `AstNode`s get matched so this doesn't
transparently happen again as new AngularAst nodes get added.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4c8ffd..c9a401a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
matching pipe. Optional arguments are not yet typechecked.
- Add typechecking support for `[attr.foo.if]`, and ensure that a corresponding
`[attr.foo]` binding exists.
+- Fixed issues with `<ng-container>`, which resulted in the inner content simply
+ being ignored instead of being validated (and also caused some problems
+ with finding inner `<ng-content>` tags).
## 0.0.17+3
diff --git a/angular_analyzer_plugin/lib/src/converter.dart b/angular_analyzer_plugin/lib/src/converter.dart
index 695685a..4e93af1 100644
--- a/angular_analyzer_plugin/lib/src/converter.dart
+++ b/angular_analyzer_plugin/lib/src/converter.dart
@@ -388,7 +388,6 @@
@required ElementInfo parent,
}) {
if (node is ElementAst) {
- final localName = node.name;
final attributes = _convertAttributes(
attributes: node.attributes,
bananas: node.bananas,
@@ -397,84 +396,35 @@
references: node.references,
stars: node.stars,
)..sort((a, b) => a.offset.compareTo(b.offset));
- final closeComponent = node.closeComplement;
- SourceRange openingSpan;
- SourceRange openingNameSpan;
- SourceRange closingSpan;
- SourceRange closingNameSpan;
- if (node.isSynthetic) {
- openingSpan = _toSourceRange(closeComponent.beginToken.offset, 0);
- openingNameSpan = openingSpan;
- } else {
- openingSpan = _toSourceRange(
- node.beginToken.offset, node.endToken.end - node.beginToken.offset);
- openingNameSpan = new SourceRange(
- (node as ParsedElementAst).identifierToken.offset,
- (node as ParsedElementAst).identifierToken.lexeme.length);
- }
- // Check for void element cases (has closing complement)
- // If closeComponent is synthetic, handle it after child nodes are found.
- if (closeComponent != null && !closeComponent.isSynthetic) {
- closingSpan = _toSourceRange(closeComponent.beginToken.offset,
- closeComponent.endToken.end - closeComponent.beginToken.offset);
- closingNameSpan =
- new SourceRange(closingSpan.offset + '</'.length, localName.length);
- }
-
- final element = new ElementInfo(
- localName,
- openingSpan,
- closingSpan,
- openingNameSpan,
- closingNameSpan,
+ return _elementInfoFromNodeAndCloseComplement(
+ node,
+ node.name,
attributes,
- findTemplateAttribute(attributes),
+ node.closeComplement,
parent,
- isTemplate: false,
);
+ } else if (node is ContainerAst) {
+ final attributes = _convertAttributes(
+ stars: node.stars,
+ )..sort((a, b) => a.offset.compareTo(b.offset));
- for (final attribute in attributes) {
- attribute.parent = element;
- }
-
- final children = _convertChildren(node, element);
- element.childNodes.addAll(children);
-
- if (!element.isSynthetic &&
- element.openingSpanIsClosed &&
- closingSpan != null &&
- (openingSpan.offset + openingSpan.length) == closingSpan.offset) {
- element.childNodes.add(new TextInfo(
- openingSpan.offset + openingSpan.length, '', element, [],
- synthetic: true));
- }
-
- return element;
- }
- if (node is EmbeddedContentAst) {
- final localName = 'ng-content';
+ return _elementInfoFromNodeAndCloseComplement(
+ node,
+ 'ng-container',
+ attributes,
+ node.closeComplement,
+ parent,
+ );
+ } else if (node is EmbeddedContentAst) {
final attributes = <AttributeInfo>[];
- final closeComplement = node.closeComplement;
- SourceRange openingSpan;
- SourceRange openingNameSpan;
- SourceRange closingSpan;
- SourceRange closingNameSpan;
- if (node.isSynthetic) {
- openingSpan = _toSourceRange(closeComplement.beginToken.offset, 0);
- openingNameSpan = openingSpan;
- } else {
- openingSpan = _toSourceRange(
- node.beginToken.offset, node.endToken.end - node.beginToken.offset);
- openingNameSpan =
- new SourceRange(openingSpan.offset + '<'.length, localName.length);
- final pnode = node as ParsedEmbeddedContentAst;
- final valueToken = pnode.selectorValueToken;
- if (pnode.selectToken != null) {
+ if (node is ParsedEmbeddedContentAst) {
+ final valueToken = node.selectorValueToken;
+ if (node.selectToken != null) {
attributes.add(new TextAttribute(
'select',
- pnode.selectToken.offset,
+ node.selectToken.offset,
valueToken?.innerValue?.lexeme,
valueToken?.innerValue?.offset,
[],
@@ -482,36 +432,14 @@
}
}
- if (closeComplement.isSynthetic) {
- closingSpan = _toSourceRange(node.endToken.end, 0);
- closingNameSpan = closingSpan;
- } else {
- closingSpan = _toSourceRange(closeComplement.beginToken.offset,
- closeComplement.endToken.end - closeComplement.beginToken.offset);
- closingNameSpan =
- new SourceRange(closingSpan.offset + '</'.length, localName.length);
- }
-
- final ngContent = new ElementInfo(
- localName,
- openingSpan,
- closingSpan,
- openingNameSpan,
- closingNameSpan,
+ return _elementInfoFromNodeAndCloseComplement(
+ node,
+ 'ng-content',
attributes,
- null,
+ node.closeComplement,
parent,
- isTemplate: false,
);
-
- for (final attribute in attributes) {
- attribute.parent = ngContent;
- }
-
- return ngContent;
- }
- if (node is EmbeddedTemplateAst) {
- final localName = 'template';
+ } else if (node is EmbeddedTemplateAst) {
final attributes = _convertAttributes(
attributes: node.attributes,
events: node.events,
@@ -519,75 +447,27 @@
references: node.references,
letBindings: node.letBindings,
);
- final closeComponent = node.closeComplement;
- SourceRange openingSpan;
- SourceRange openingNameSpan;
- SourceRange closingSpan;
- SourceRange closingNameSpan;
- if (node.isSynthetic) {
- openingSpan = _toSourceRange(closeComponent.beginToken.offset, 0);
- openingNameSpan = openingSpan;
- } else {
- openingSpan = _toSourceRange(
- node.beginToken.offset, node.endToken.end - node.beginToken.offset);
- openingNameSpan =
- new SourceRange(openingSpan.offset + '<'.length, localName.length);
- }
- // Check for void element cases (has closing complement)
- if (closeComponent != null) {
- if (closeComponent.isSynthetic) {
- closingSpan = _toSourceRange(node.endToken.end, 0);
- closingNameSpan = closingSpan;
- } else {
- closingSpan = _toSourceRange(closeComponent.beginToken.offset,
- closeComponent.endToken.end - closeComponent.beginToken.offset);
- closingNameSpan = new SourceRange(
- closingSpan.offset + '</'.length, localName.length);
- }
- }
-
- final element = new ElementInfo(
- localName,
- openingSpan,
- closingSpan,
- openingNameSpan,
- closingNameSpan,
+ return _elementInfoFromNodeAndCloseComplement(
+ node,
+ 'template',
attributes,
- findTemplateAttribute(attributes),
+ node.closeComplement,
parent,
- isTemplate: true,
);
-
- for (final attribute in attributes) {
- attribute.parent = element;
- }
-
- final children = _convertChildren(node, element);
- element.childNodes.addAll(children);
-
- if (!element.isSynthetic &&
- element.openingSpanIsClosed &&
- closingSpan != null &&
- (openingSpan.offset + openingSpan.length) == closingSpan.offset) {
- element.childNodes.add(new TextInfo(
- openingSpan.offset + openingSpan.length, '', element, [],
- synthetic: true));
- }
-
- return element;
- }
- if (node is TextAst) {
+ } else if (node is TextAst) {
final offset = node.sourceSpan.start.offset;
final text = node.value;
return new TextInfo(
offset, text, parent, dartParser.findMustaches(text, offset));
- }
- if (node is InterpolationAst) {
+ } else if (node is InterpolationAst) {
final offset = node.sourceSpan.start.offset;
final text = '{{${node.value}}}';
return new TextInfo(
offset, text, parent, dartParser.findMustaches(text, offset));
+ } else {
+ assert(
+ node is CommentAst, 'Unknown node type ${node.runtimeType} ($node)');
}
return null;
}
@@ -903,6 +783,80 @@
return templateAttribute;
}
+ /// There are four types of "tags" in angular_ast which don't implement a
+ /// common interface. But we need to generate source spans for all of them.
+ /// We can do this if we have the [node] (we can use its [beginToken]) and its
+ /// [closeComplement]. So this takes that info, plus a few other structural
+ /// things ([attributes], [parent], [tagName]), to handle all four cases.
+ ElementInfo _elementInfoFromNodeAndCloseComplement(
+ StandaloneTemplateAst node,
+ String tagName,
+ List<AttributeInfo> attributes,
+ CloseElementAst closeComplement,
+ ElementInfo parent) {
+ final isTemplate = tagName == 'template';
+ SourceRange openingSpan;
+ SourceRange openingNameSpan;
+ SourceRange closingSpan;
+ SourceRange closingNameSpan;
+
+ if (node.isSynthetic && closeComplement != null) {
+ // This code assumes that a synthetic node is a close tag with no open
+ // tag, ie, a dangling `</div>`.
+ openingSpan = _toSourceRange(closeComplement.beginToken.offset, 0);
+ openingNameSpan ??= openingSpan;
+ } else {
+ openingSpan = _toSourceRange(
+ node.beginToken.offset, node.endToken.end - node.beginToken.offset);
+ openingNameSpan ??=
+ new SourceRange(node.beginToken.offset + '<'.length, tagName.length);
+ }
+
+ if (closeComplement != null) {
+ if (!closeComplement.isSynthetic) {
+ closingSpan = _toSourceRange(closeComplement.beginToken.offset,
+ closeComplement.endToken.end - closeComplement.beginToken.offset);
+ closingNameSpan =
+ new SourceRange(closingSpan.offset + '</'.length, tagName.length);
+ } else {
+ // TODO(mfairhurst): generate a closingSpan for synthetic tags too. This
+ // can mess up autocomplete if we do it wrong.
+ }
+ }
+
+ final element = new ElementInfo(
+ tagName,
+ openingSpan,
+ closingSpan,
+ openingNameSpan,
+ closingNameSpan,
+ attributes,
+ findTemplateAttribute(attributes),
+ parent,
+ isTemplate: isTemplate,
+ );
+
+ for (final attribute in attributes) {
+ attribute.parent = element;
+ }
+
+ final children = _convertChildren(node, element);
+ element.childNodes.addAll(children);
+
+ // For empty tags, ie, `<div></div>`, generate a synthetic text entry
+ // between the two tags. This simplifies later autocomplete code.
+ if (!element.isSynthetic &&
+ element.openingSpanIsClosed &&
+ closingSpan != null &&
+ (openingSpan.offset + openingSpan.length) == closingSpan.offset) {
+ element.childNodes.add(new TextInfo(
+ openingSpan.offset + openingSpan.length, '', element, [],
+ synthetic: true));
+ }
+
+ return element;
+ }
+
SourceRange _toSourceRange(int offset, int length) =>
new SourceRange(offset, length);
}
diff --git a/angular_analyzer_plugin/lib/src/resolver.dart b/angular_analyzer_plugin/lib/src/resolver.dart
index cf964a0..7b058b4 100644
--- a/angular_analyzer_plugin/lib/src/resolver.dart
+++ b/angular_analyzer_plugin/lib/src/resolver.dart
@@ -370,7 +370,9 @@
bool _isStandardTagName(String name) {
// ignore: parameter_assignments
name = name.toLowerCase();
- return !name.contains('-') || name == 'ng-content';
+ return !name.contains('-') ||
+ name == 'ng-content' ||
+ name == 'ng-container';
}
void _reportErrorForRange(SourceRange range, ErrorCode errorCode,
diff --git a/angular_analyzer_plugin/test/resolver_test.dart b/angular_analyzer_plugin/test/resolver_test.dart
index 2900752..0a9d030 100644
--- a/angular_analyzer_plugin/test/resolver_test.dart
+++ b/angular_analyzer_plugin/test/resolver_test.dart
@@ -2651,6 +2651,21 @@
}
// ignore: non_constant_identifier_names
+ Future test_ngContainer_withStar() async {
+ _addDartSource(r'''
+@Component(selector: 'test-panel', templateUrl: 'test_panel.html')
+class TestPanel {}
+''');
+ final htmlCode = r"""
+<ng-container *foo></ng-container>>
+""";
+ _addHtmlSource(htmlCode);
+ await _resolveSingleTemplate(dartSource);
+ assertErrorInCodeAtPosition(
+ AngularWarningCode.TEMPLATE_ATTR_NOT_USED, htmlCode, '*foo');
+ }
+
+ // ignore: non_constant_identifier_names
Future test_ngContent() async {
_addDartSource(r'''
@Component(selector: 'test-panel', templateUrl: 'test_panel.html')