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')