Feedback from Paul: Don't use tuple, ambiguous pipes, etc (#630)

- Don't use tuple
- Ambiguous pipe error message
- replace `parseIdentifierList().first` with the intended,
  `parseSimpleIdentifier()`
- unnecessary null check
diff --git a/angular_analyzer_plugin/lib/errors.dart b/angular_analyzer_plugin/lib/errors.dart
index c373786..50e964e 100644
--- a/angular_analyzer_plugin/lib/errors.dart
+++ b/angular_analyzer_plugin/lib/errors.dart
@@ -65,6 +65,7 @@
   AngularWarningCode.PIPE_TRANSFORM_NO_NAMED_ARGS,
   AngularWarningCode.PIPE_TRANSFORM_REQ_ONE_ARG,
   AngularWarningCode.PIPE_NOT_FOUND,
+  AngularWarningCode.AMBIGUOUS_PIPE,
   AngularWarningCode.UNSAFE_BINDING,
   AngularWarningCode.EVENT_REDUCTION_NOT_ALLOWED,
   AngularWarningCode.FUNCTIONAL_DIRECTIVES_CANT_BE_EXPORTED,
@@ -498,6 +499,13 @@
       "Pipe by name of {0} not found. Did you reference it in your @Component"
       " configuration?");
 
+  /// An error indicating that pipe syntax was used in an angular template, but
+  /// the name of the pipe doesn't match one defined in the component
+  static const AMBIGUOUS_PIPE = const AngularWarningCode(
+      'AMBIGUOUS_PIPE',
+      "Multiple pipes by name of {0} found. Check the `pipes` field of your "
+      "@Component annotation for duplicates and/or conflicts.");
+
   /// An error indicating that a security exception will be thrown by this input
   /// binding
   static const UNSAFE_BINDING = const AngularWarningCode(
diff --git a/angular_analyzer_plugin/lib/src/ng_expr_parser.dart b/angular_analyzer_plugin/lib/src/ng_expr_parser.dart
index 2202537..1c010e1 100644
--- a/angular_analyzer_plugin/lib/src/ng_expr_parser.dart
+++ b/angular_analyzer_plugin/lib/src/ng_expr_parser.dart
@@ -13,36 +13,42 @@
 
   Token get _currentToken => super.currentToken;
 
-  /// Parse a bitwise or expression to be treated as a pipe.
-  /// Return the resolved left-hand expression as a dynamic type.
+  /// Override the bitwise or operator to parse pipes instead
+  @override
+  Expression parseBitwiseOrExpression() => parsePipeExpression();
+
+  /// Parse pipe expression. Return the result as a cast expression `as dynamic`
+  /// with _ng_pipeXXX properties to be resolved specially later.
   ///
   ///     bitwiseOrExpression ::=
-  ///         bitwiseXorExpression ('|' pipeExpression)*
-  @override
-  Expression parseBitwiseOrExpression() {
+  ///         bitwiseXorExpression ('|' pipeName [: arg]*)*
+  Expression parsePipeExpression() {
     Expression expression;
     Token beforePipeToken;
     expression = parseBitwiseXorExpression();
     while (_currentToken.type == TokenType.BAR) {
       beforePipeToken ??= _currentToken.previous;
       getAndAdvance();
-      final pipeData = parsePipeExpression();
-      if (beforePipeToken != null) {
-        final asToken = new KeywordToken(Keyword.AS, 0);
-        final dynamicIdToken = new SyntheticStringToken(TokenType.IDENTIFIER,
-            "dynamic", _currentToken.offset - "dynamic".length);
+      final pipeEntities = parsePipeExpressionEntities();
+      final asToken = new KeywordToken(Keyword.AS, 0);
+      final dynamicIdToken = new SyntheticStringToken(TokenType.IDENTIFIER,
+          "dynamic", _currentToken.offset - "dynamic".length);
 
-        beforePipeToken.setNext(asToken);
-        asToken.setNext(dynamicIdToken);
-        dynamicIdToken.setNext(_currentToken);
+      beforePipeToken.setNext(asToken);
+      asToken.setNext(dynamicIdToken);
+      dynamicIdToken.setNext(_currentToken);
 
-        final dynamicIdentifier = astFactory.simpleIdentifier(dynamicIdToken);
+      final dynamicIdentifier = astFactory.simpleIdentifier(dynamicIdToken);
 
-        expression = astFactory.asExpression(
-            expression, asToken, astFactory.typeName(dynamicIdentifier, null))
-          ..setProperty('_ng_pipeName', pipeData.item1)
-          ..setProperty('_ng_pipeArgs', pipeData.item2);
-      }
+      // TODO(mfairhurst) Now that we are resolving pipes, probably should store
+      // the result in a different expression type -- a function call, most
+      // likely. This will be required so that the pipeArgs become part of the
+      // tree, but it may create secondary fallout inside the analyzer
+      // resolution code if done wrong.
+      expression = astFactory.asExpression(
+          expression, asToken, astFactory.typeName(dynamicIdentifier, null))
+        ..setProperty('_ng_pipeName', pipeEntities.name)
+        ..setProperty('_ng_pipeArgs', pipeEntities.arguments);
     }
     return expression;
   }
@@ -51,15 +57,21 @@
   /// Return the resolved left-hand expression as a dynamic type.
   ///
   ///     pipeExpression ::= identifier[':' expression]*
-  Tuple2<SimpleIdentifier, List<Expression>> parsePipeExpression() {
-    final identifiers = parseIdentifierList();
+  _PipeEntities parsePipeExpressionEntities() {
+    final identifier = parseSimpleIdentifier();
     final expressions = <Expression>[];
     while (_currentToken.type == TokenType.COLON) {
       getAndAdvance();
       expressions.add(parseExpression2());
     }
 
-    return Tuple2<SimpleIdentifier, List<Expression>>(
-        identifiers.first, expressions);
+    return _PipeEntities(identifier, expressions);
   }
 }
+
+class _PipeEntities {
+  final SimpleIdentifier name;
+  final List<Expression> arguments;
+
+  _PipeEntities(this.name, this.arguments);
+}
diff --git a/angular_analyzer_plugin/lib/src/resolver.dart b/angular_analyzer_plugin/lib/src/resolver.dart
index 7b058b4..1aa4a94 100644
--- a/angular_analyzer_plugin/lib/src/resolver.dart
+++ b/angular_analyzer_plugin/lib/src/resolver.dart
@@ -121,9 +121,12 @@
       final pipeName = exp.getProperty<SimpleIdentifier>('_ng_pipeName');
       final matchingPipes =
           pipes.where((pipe) => pipe.pipeName == pipeName.name);
-      if (matchingPipes.length != 1) {
+      if (matchingPipes.isEmpty) {
         errorReporter.reportErrorForNode(
             AngularWarningCode.PIPE_NOT_FOUND, pipeName, [pipeName]);
+      } else if (matchingPipes.length > 1) {
+        errorReporter.reportErrorForNode(
+            AngularWarningCode.AMBIGUOUS_PIPE, pipeName, [pipeName]);
       } else {
         final matchingPipe = matchingPipes.single;
         exp.staticType = matchingPipe.transformReturnType;
diff --git a/angular_analyzer_plugin/test/resolver_test.dart b/angular_analyzer_plugin/test/resolver_test.dart
index 0a9d030..e4cf391 100644
--- a/angular_analyzer_plugin/test/resolver_test.dart
+++ b/angular_analyzer_plugin/test/resolver_test.dart
@@ -1663,6 +1663,33 @@
   }
 
   // ignore: non_constant_identifier_names
+  Future test_expression_pipe_in_moustache_ambiguous() async {
+    _addDartSource(r'''
+@Component(selector: 'test-panel', templateUrl: 'test_panel.html',
+    pipes: [AmbiguousPipe1, AmbiguousPipe2])
+class TestPanel {
+}
+
+@Pipe('ambiguous')
+class AmbiguousPipe1 extends PipeTransform {
+  String transform(String x => "";
+}
+
+@Pipe('ambiguous')
+class AmbiguousPipe2 extends PipeTransform {
+  String transform(String x => "";
+}
+''');
+    final code = r"""
+{{"" | ambiguous}}
+""";
+    _addHtmlSource(code);
+    await _resolveSingleTemplate(dartSource);
+    assertErrorInCodeAtPosition(
+        AngularWarningCode.AMBIGUOUS_PIPE, code, 'ambiguous');
+  }
+
+  // ignore: non_constant_identifier_names
   @failingTest
   Future test_expression_pipe_in_moustache_extraArg() async {
     _addDartSource(r'''