Repository: aws-cloudformation/cfn-lint

Issue:
Only the first Resource Duplicate is found - E0000
*cfn-lint version: (`cfn-lint 0.44.3`)*

*Description of issue.*

I get the error `E0000 Duplicate resource found "DevRouteTableRoute2" (line 18)` when I try to validate a CFn template with duplicated resources. I should get has many errors has the number of duplicated resources on the template. The template had more than one duplication, but the linter only showed the 1st.

Please provide as much information as possible:
* Test Template:
```yaml
---
AWSTemplateFormatVersion: "2010-09-09"

Resources:

  DevRouteTableRoute1:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      Blackhole: true
      DestinationCidrBlock: 10.1.1.0/24
      TransitGatewayRouteTableId: tgw-rtb-d65d85280f2c9e51c
  DevRouteTableRoute2:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.2.0/24
      TransitGatewayAttachmentId: tgw-attach-9bdf1fdfb44d9c7d7
      TransitGatewayRouteTableId: tgw-rtb-d65d85280f2c9e51c
  DevRouteTableRoute2:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.3.0/24
      TransitGatewayAttachmentId: tgw-attach-b16ea962254d4373e
      TransitGatewayRouteTableId: tgw-rtb-d65d85280f2c9e51c
  DevRouteTableRoute4:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.4.0/24
      TransitGatewayAttachmentId: tgw-attach-b16ea962254d4373e
      TransitGatewayRouteTableId: tgw-rtb-d65d85280f2c9e51c

  ProdRouteTableRoute1:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      Blackhole: true
      DestinationCidrBlock: 10.1.1.0/24
      TransitGatewayRouteTableId: tgw-rtb-5e9891f700a3af932
  ProdRouteTableRoute2:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.2.0/24
      TransitGatewayAttachmentId: tgw-attach-9bdf1fdfb44d9c7d7
      TransitGatewayRouteTableId: tgw-rtb-5e9891f700a3af932
  ProdRouteTableRoute2:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.3.0/24
      TransitGatewayAttachmentId: tgw-attach-b16ea962254d4373e
      TransitGatewayRouteTableId: tgw-rtb-5e9891f700a3af932
  ProdRouteTableRoute4:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      DestinationCidrBlock: 10.1.4.0/24
      TransitGatewayAttachmentId: tgw-attach-b16ea962254d4373e
      TransitGatewayRouteTableId: tgw-rtb-5e9891f700a3af932
```

* Validation Output
```
cfn-lint -t template.yml
E0000 Duplicate resource found "DevRouteTableRoute2" (line 18)
template.yml:18:3
```

* Expected Output
```
cfn-lint -t template.yml
E0000 Duplicate resource found "DevRouteTableRoute2" (line 18)
template.yml:18:3

E0000 Duplicate resource found "ProdRouteTableRoute2" (line 43)
template.yml:43:3
```

I've run manually cfn-lint -u but the validation on cfn-lint only shows on duplicate instead of the two that exist in the template.



Discussion:

Resolving patch:
diff --git a/src/cfnlint/decode/__init__.py b/src/cfnlint/decode/__init__.py
--- a/src/cfnlint/decode/__init__.py
+++ b/src/cfnlint/decode/__init__.py
@@ -51,8 +51,7 @@ def decode(filename):
         matches.append(create_match_file_error(
             filename, 'Cannot read file contents: %s' % filename))
     except cfn_yaml.CfnParseError as err:
-        err.match.Filename = filename
-        matches = [err.match]
+        matches = err.matches
     except ParserError as err:
         matches = [create_match_yaml_parser_error(err, filename)]
     except ScannerError as err:
@@ -63,8 +62,9 @@ def decode(filename):
             try:
                 template = cfn_json.load(filename)
             except cfn_json.JSONDecodeError as json_err:
-                json_err.match.filename = filename
-                matches = [json_err.match]
+                for e in json_err.matches:
+                    e.filename = filename
+                matches = json_err.matches
             except JSONDecodeError as json_err:
                 if hasattr(json_err, 'message'):
                     if json_err.message == 'No JSON object could be decoded':  # pylint: disable=no-member
diff --git a/src/cfnlint/decode/cfn_json.py b/src/cfnlint/decode/cfn_json.py
--- a/src/cfnlint/decode/cfn_json.py
+++ b/src/cfnlint/decode/cfn_json.py
@@ -20,6 +20,11 @@ class DuplicateError(Exception):
     Error thrown when the template contains duplicates
     """
 
+    def __init__(self, message, mapping, key):
+        super(DuplicateError, self).__init__(message)
+
+        self.mapping = mapping
+        self.key = key
 
 class NullError(Exception):
     """
@@ -38,7 +43,7 @@ def check_duplicates(ordered_pairs, beg_mark, end_mark):
         if value is None:
             raise NullError('"{}"'.format(key))
         if key in mapping:
-            raise DuplicateError('"{}"'.format(key))
+            raise DuplicateError('"{}"'.format(key), mapping, key)
         mapping[key] = value
     return mapping
 
@@ -53,24 +58,40 @@ class JSONDecodeError(ValueError):
     """
     # Note that this exception is used from _json
 
-    def __init__(self, msg, doc, pos, key=' '):
-        lineno = doc.count('\n', 0, pos) + 1
-        colno = pos - doc.rfind('\n', 0, pos)
-        errmsg = '%s: line %d column %d (char %d)' % (msg, lineno, colno, pos)
+    def __init__(self, doc, pos, errors):
+
+        if isinstance(errors, cfnlint.rules.Match):
+            errors = [errors]
+
+        errmsg = '%s: line %d column %d (char %d)' % (errors[0].message, errors[0].linenumber, errors[0].linenumber, pos)
         ValueError.__init__(self, errmsg)
-        self.msg = msg
+        self.msg = errors[0].message
         self.doc = doc
         self.pos = pos
-        self.lineno = lineno
-        self.colno = colno
-        self.match = cfnlint.rules.Match(
-            lineno, colno + 1, lineno,
-            colno + 1 + len(key), '', cfnlint.rules.ParseError(), message=msg)
+        self.lineno = errors[0].linenumber
+        self.colno = errors[0].linenumber
+        self.matches = errors
 
     def __reduce__(self):
         return self.__class__, (self.msg, self.doc, self.pos)
 
 
+def build_match(message, doc, pos, key=' '):
+
+    lineno = doc.count('\n', 0, pos) + 1
+    colno = pos - doc.rfind('\n', 0, pos)
+
+    return cfnlint.rules.Match(
+        lineno, colno + 1, lineno,
+        colno + 1 + len(key), '', cfnlint.rules.ParseError(), message=message)
+
+
+def build_match_from_node(message, node, key):
+
+    return cfnlint.rules.Match(
+        node[key].start_mark.line, node[key].start_mark.column + 1, node[key].end_mark.line,
+        node[key].end_mark.column + 1 + len(key), '', cfnlint.rules.ParseError(), message=message)
+
 class Mark(object):
     """Mark of line and column"""
     line = 1
@@ -98,7 +119,17 @@ def py_scanstring(s, end, strict=True,
     while 1:
         chunk = _m(s, end)
         if chunk is None:
-            raise JSONDecodeError('Unterminated string starting at', s, begin)
+            raise JSONDecodeError(
+                doc=s,
+                pos=begin,
+                errors=[
+                    build_match(
+                        message='Unterminated string starting at',
+                        doc=s,
+                        pos=begin,
+                    ),
+                ]
+            )
         end = chunk.end()
         content, terminator = chunk.groups()
         # Content is contains zero or more unescaped string characters
@@ -111,20 +142,50 @@ def py_scanstring(s, end, strict=True,
         if terminator != '\\':
             if strict:
                 msg = 'Invalid control character {0!r} at'.format(terminator)
-                raise JSONDecodeError(msg, s, end)
+                raise JSONDecodeError(
+                    doc=s,
+                    pos=end,
+                    errors=[
+                        build_match(
+                            message=msg,
+                            doc=s,
+                            pos=end,
+                        ),
+                    ]
+                )
             _append(terminator)
             continue
         try:
             esc = s[end]
         except IndexError:
-            raise JSONDecodeError('Unterminated string starting at', s, begin)
+            raise JSONDecodeError(
+                doc=s,
+                pos=begin,
+                errors=[
+                    build_match(
+                        message='Unterminated string starting at',
+                        doc=s,
+                        pos=begin,
+                    ),
+                ]
+            )
         # If not a unicode escape sequence, must be in the lookup table
         if esc != 'u':
             try:
                 char = _b[esc]
             except KeyError:
                 msg = 'Invalid \\escape: {0!r}'.format(esc)
-                raise JSONDecodeError(msg, s, end)
+                raise JSONDecodeError(
+                    doc=s,
+                    pos=end,
+                    errors=[
+                        build_match(
+                            message=msg,
+                            doc=s,
+                            pos=end,
+                        ),
+                    ]
+                )
             end += 1
         else:
             uni = _decode_uXXXX(s, end)
@@ -151,7 +212,17 @@ def _decode_uXXXX(s, pos):
         except ValueError:
             pass
     msg = 'Invalid \\uXXXX escape'
-    raise JSONDecodeError(msg, s, pos)
+    raise JSONDecodeError(
+        doc=s,
+        pos=pos,
+        errors=[
+            build_match(
+                message=msg,
+                doc=s,
+                pos=pos,
+            ),
+        ]
+    )
 
 
 def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
@@ -181,9 +252,34 @@ def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
                     result = object_pairs_hook(pairs, beg_mark, end_mark)
                     return result, end + 1
                 except DuplicateError as err:
-                    raise JSONDecodeError('Duplicate found {}'.format(err), s, end)
+                    raise JSONDecodeError(
+                        doc=s,
+                        pos=end,
+                        errors=[
+                            build_match_from_node(
+                                message='Duplicate found {}'.format(err),
+                                node=err.mapping,
+                                key=err.key,
+                            ),
+                            build_match(
+                                message='Duplicate found {}'.format(err),
+                                doc=s,
+                                pos=end,
+                            ),
+                        ]
+                    )
                 except NullError as err:
-                    raise JSONDecodeError('Null Error {}'.format(err), s, end)
+                    raise JSONDecodeError(
+                        doc=s,
+                        pos=end,
+                        errors=[
+                            build_match(
+                                message='Null Error {}'.format(err),
+                                doc=s,
+                                pos=end,
+                            ),
+                        ]
+                    )
             pairs = {}
             if object_hook is not None:
                 beg_mark, end_mark = get_beg_end_mark(s, original_end, end + 1)
@@ -191,7 +287,17 @@ def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
             return pairs, end + 1
 
         if nextchar != '"':
-            raise JSONDecodeError('Expecting property name enclosed in double quotes', s, end)
+            raise JSONDecodeError(
+                doc=s,
+                pos=end,
+                errors=[
+                    build_match(
+                        message='Expecting property name enclosed in double quotes',
+                        doc=s,
+                        pos=end,
+                    ),
+                ]
+            )
     end += 1
     while True:
         begin = end - 1
@@ -204,7 +310,17 @@ def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
         if s[end:end + 1] != ':':
             end = _w(s, end).end()
             if s[end:end + 1] != ':':
-                raise JSONDecodeError('Expecting \':\' delimiter', s, end)
+                raise JSONDecodeError(
+                    doc=s,
+                    pos=end,
+                    errors=[
+                        build_match(
+                            message='Expecting \':\' delimiter',
+                            doc=s,
+                            pos=end,
+                        ),
+                    ]
+                )
         end += 1
 
         try:
@@ -219,7 +335,17 @@ def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
         try:
             value, end = scan_once(s, end)
         except StopIteration as err:
-            raise JSONDecodeError('Expecting value', s, str(err))
+            raise JSONDecodeError(
+                doc=s,
+                pos=str(err),
+                errors=[
+                    build_match(
+                        message='Expecting value',
+                        doc=s,
+                        pos=str(err),
+                    ),
+                ]
+            )
         key_str = str_node(key, beg_mark, end_mark)
         pairs_append((key_str, value))
         try:
@@ -234,21 +360,65 @@ def CfnJSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook,
         if nextchar == '}':
             break
         if nextchar != ',':
-            raise JSONDecodeError('Expecting \',\' delimiter', s, end - 1)
+            raise JSONDecodeError(
+                doc=s,
+                pos=end - 1,
+                errors=[
+                    build_match(
+                        message='Expecting \',\' delimiter',
+                        doc=s,
+                        pos=end - 1,
+                    ),
+                ]
+            )
         end = _w(s, end).end()
         nextchar = s[end:end + 1]
         end += 1
         if nextchar != '"':
             raise JSONDecodeError(
-                'Expecting property name enclosed in double quotes', s, end - 1)
+                doc=s,
+                pos=end - 1,
+                errors=[
+                    build_match(
+                        message='Expecting property name enclosed in double quotes',
+                        doc=s,
+                        pos=end - 1,
+                    ),
+                ]
+            )
     if object_pairs_hook is not None:
         try:
             beg_mark, end_mark = get_beg_end_mark(s, original_end, end)
             result = object_pairs_hook(pairs, beg_mark, end_mark)
         except DuplicateError as err:
-            raise JSONDecodeError('Duplicate found {}'.format(err), s, begin, key)
+            raise JSONDecodeError(
+                doc=s,
+                pos=end,
+                errors=[
+                    build_match_from_node(
+                        message='Duplicate found {}'.format(err),
+                        node=err.mapping,
+                        key=err.key,
+                    ),
+                    build_match(
+                        message='Duplicate found {}'.format(err),
+                        doc=s,
+                        pos=end,
+                    ),
+                ]
+            )
         except NullError as err:
-            raise JSONDecodeError('Null Error {}'.format(err), s, begin, key)
+            raise JSONDecodeError(
+                doc=s,
+                pos=end,
+                errors=[
+                    build_match(
+                        message='Null Error {}'.format(err),
+                        doc=s,
+                        pos=end,
+                    ),
+                ]
+            )
         return result, end
 
     pairs = dict(pairs)
diff --git a/src/cfnlint/decode/cfn_yaml.py b/src/cfnlint/decode/cfn_yaml.py
--- a/src/cfnlint/decode/cfn_yaml.py
+++ b/src/cfnlint/decode/cfn_yaml.py
@@ -36,20 +36,22 @@ class CfnParseError(ConstructorError):
     Error thrown when the template contains Cfn Error
     """
 
-    def __init__(self, filename, message, line_number, column_number, key=' '):
+    def __init__(self, filename, errors):
+
+        if isinstance(errors, cfnlint.rules.Match):
+            errors = [errors]
 
         # Call the base class constructor with the parameters it needs
-        super(CfnParseError, self).__init__(message)
+        super(CfnParseError, self).__init__(errors[0].message)
 
         # Now for your custom code...
         self.filename = filename
-        self.line_number = line_number
-        self.column_number = column_number
-        self.message = message
-        self.match = cfnlint.rules.Match(
-            line_number + 1, column_number + 1, line_number + 1,
-            column_number + 1 + len(key), filename, cfnlint.rules.ParseError(), message=message)
+        self.matches = errors
 
+def build_match(filename, message, line_number, column_number, key):
+    return cfnlint.rules.Match(
+        line_number + 1, column_number + 1, line_number + 1,
+        column_number + 1 + len(key), filename, cfnlint.rules.ParseError(), message=message)
 
 class NodeConstructor(SafeConstructor):
     """
@@ -81,9 +83,25 @@ def construct_yaml_map(self, node):
             if key in mapping:
                 raise CfnParseError(
                     self.filename,
-                    'Duplicate resource found "{}" (line {})'.format(
-                        key, key_node.start_mark.line + 1),
-                    key_node.start_mark.line, key_node.start_mark.column, key)
+                    [
+                        build_match(
+                            filename=self.filename,
+                            message='Duplicate resource found "{}" (line {})'.format(
+                                key, key_node.start_mark.line + 1),
+                            line_number=key_node.start_mark.line,
+                            column_number=key_node.start_mark.column,
+                            key=key
+                        ),
+                        build_match(
+                            filename=self.filename,
+                            message='Duplicate resource found "{}" (line {})'.format(
+                                key, mapping[key].start_mark.line + 1),
+                            line_number=mapping[key].start_mark.line,
+                            column_number=mapping[key].start_mark.column,
+                            key=key
+                        )
+                    ]
+                )
             mapping[key] = value
 
         obj, = SafeConstructor.construct_yaml_map(self, node)
@@ -103,9 +121,17 @@ def construct_yaml_null_error(self, node):
         """Throw a null error"""
         raise CfnParseError(
             self.filename,
-            'Null value at line {0} column {1}'.format(
-                node.start_mark.line + 1, node.start_mark.column + 1),
-            node.start_mark.line, node.start_mark.column, ' ')
+            [
+                build_match(
+                    filename=self.filename,
+                    message='Null value at line {0} column {1}'.format(
+                        node.start_mark.line + 1, node.start_mark.column + 1),
+                    line_number=node.start_mark.line,
+                    column_number=node.start_mark.column,
+                    key=' ',
+                )
+            ]
+        )
 
 
 NodeConstructor.add_constructor(


Test patch:
diff --git a/test/unit/module/test_duplicate.py b/test/unit/module/test_duplicate.py
--- a/test/unit/module/test_duplicate.py
+++ b/test/unit/module/test_duplicate.py
@@ -44,8 +44,8 @@ def test_fail_run(self):
         try:
             with open(filename) as fp:
                 json.load(fp, cls=cfnlint.decode.cfn_json.CfnJSONDecoder)
-        except cfnlint.decode.cfn_json.JSONDecodeError:
-            assert(True)
+        except cfnlint.decode.cfn_json.JSONDecodeError as e:
+            self.assertEqual(len(e.matches), 2)
             return
 
         assert(False)
@@ -57,8 +57,8 @@ def test_fail_yaml_run(self):
 
         try:
             cfnlint.decode.cfn_yaml.load(filename)
-        except cfnlint.decode.cfn_yaml.CfnParseError:
-            assert(True)
+        except cfnlint.decode.cfn_yaml.CfnParseError as e:
+            self.assertEqual(len(e.matches), 2)
             return
 
         assert(False)