We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[cfn-lint] E0002:Unknown exception while processing rule E2541: unhashable type: 'dict_node'
*cfn-lint version: 0.16.0

I got the error message:

`[cfn-lint] E0002:Unknown exception while processing rule E2541: unhashable type: 'dict_node'
`

If I put a "!Ref Name" in the source action Name: see below
      
```
Stages:
        - Name: GitHub
          Actions:
          - Name: !Ref GitHubSourceRepo1
            ActionTypeId:
              Category: Source
              Owner: Custom
              Version: 1
              Provider: GitHUBcustom
.
.
.
.
```

If I remove  the !Ref, the cfn-lint works fine.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py`
Content:
```
1 """
2   Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 
4   Permission is hereby granted, free of charge, to any person obtaining a copy of this
5   software and associated documentation files (the "Software"), to deal in the Software
6   without restriction, including without limitation the rights to use, copy, modify,
7   merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8   permit persons to whom the Software is furnished to do so.
9 
10   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12   PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 """
17 from cfnlint import CloudFormationLintRule
18 from cfnlint import RuleMatch
19 
20 
21 class CodepipelineStageActions(CloudFormationLintRule):
22     """Check if CodePipeline Stage Actions are set up properly."""
23     id = 'E2541'
24     shortdesc = 'CodePipeline Stage Actions'
25     description = 'See if CodePipeline stage actions are set correctly'
26     source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'
27     tags = ['resources', 'codepipeline']
28 
29     CONSTRAINTS = {
30         'AWS': {
31             'Source': {
32                 'S3': {
33                     'InputArtifactRange': 0,
34                     'OutputArtifactRange': 1,
35                 },
36                 'CodeCommit': {
37                     'InputArtifactRange': 0,
38                     'OutputArtifactRange': 1,
39                 }
40             },
41             'Test': {
42                 'CodeBuild': {
43                     'InputArtifactRange': (1, 5),
44                     'OutputArtifactRange': (0, 5),
45                 }
46             },
47             'Approval': {
48                 'Manual': {
49                     'InputArtifactRange': 0,
50                     'OutputArtifactRange': 0,
51                 }
52             },
53             'Deploy': {
54                 'CloudFormation': {
55                     'InputArtifactRange': (0, 10),
56                     'OutputArtifactRange': (0, 1),
57                 },
58                 'CodeDeploy': {
59                     'InputArtifactRange': 1,
60                     'OutputArtifactRange': 0,
61                 },
62                 'ElasticBeanstalk': {
63                     'InputArtifactRange': 1,
64                     'OutputArtifactRange': 0,
65                 },
66                 'OpsWorks': {
67                     'InputArtifactRange': 1,
68                     'OutputArtifactRange': 0,
69                 },
70                 'ECS': {
71                     'InputArtifactRange': 1,
72                     'OutputArtifactRange': 0,
73                 },
74             },
75             'Invoke': {
76                 'Lambda': {
77                     'InputArtifactRange': (0, 5),
78                     'OutputArtifactRange': (0, 5),
79                 }
80             }
81         },
82         'ThirdParty': {
83             'Source': {
84                 'GitHub': {
85                     'InputArtifactRange': 0,
86                     'OutputArtifactRange': 1,
87                 }
88             },
89         },
90     }
91 
92     KEY_MAP = {
93         'InputArtifacts': 'InputArtifactRange',
94         'OutputArtifacts': 'OutputArtifactRange',
95     }
96 
97     def check_artifact_counts(self, action, artifact_type, path):
98         """Check that artifact counts are within valid ranges."""
99         matches = []
100 
101         action_type_id = action.get('ActionTypeId')
102         owner = action_type_id.get('Owner')
103         category = action_type_id.get('Category')
104         provider = action_type_id.get('Provider')
105 
106         if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):
107             self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')
108             return matches
109 
110         constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})
111         if not constraints:
112             return matches
113         artifact_count = len(action.get(artifact_type, []))
114 
115         constraint_key = self.KEY_MAP[artifact_type]
116         if isinstance(constraints[constraint_key], tuple):
117             min_, max_ = constraints[constraint_key]
118             if not (min_ <= artifact_count <= max_):
119                 message = (
120                     'Action "{action}" declares {number} {artifact_type} which is not in '
121                     'expected range [{a}, {b}].'
122                 ).format(
123                     action=action['Name'],
124                     number=artifact_count,
125                     artifact_type=artifact_type,
126                     a=min_,
127                     b=max_
128                 )
129                 matches.append(RuleMatch(
130                     path + [artifact_type],
131                     message
132                 ))
133         else:
134             if artifact_count != constraints[constraint_key]:
135                 message = (
136                     'Action "{action}" declares {number} {artifact_type} which is not the '
137                     'expected number [{a}].'
138                 ).format(
139                     action=action['Name'],
140                     number=artifact_count,
141                     artifact_type=artifact_type,
142                     a=constraints[constraint_key]
143                 )
144                 matches.append(RuleMatch(
145                     path + [artifact_type],
146                     message
147                 ))
148 
149         return matches
150 
151     def check_version(self, action, path):
152         """Check that action type version is valid."""
153         matches = []
154 
155         version = action.get('ActionTypeId', {}).get('Version')
156         if isinstance(version, dict):
157             self.logger.debug('Unable to validate version when an object is used.  Skipping')
158         elif version != '1':
159             message = 'For all currently supported action types, the only valid version string is "1".'
160             matches.append(RuleMatch(
161                 path + ['ActionTypeId', 'Version'],
162                 message
163             ))
164         return matches
165 
166     def check_names_unique(self, action, path, action_names):
167         """Check that action names are unique."""
168         matches = []
169 
170         if action.get('Name') in action_names:
171             message = 'All action names within a stage must be unique. ({name})'.format(
172                 name=action.get('Name')
173             )
174             matches.append(RuleMatch(path + ['Name'], message))
175         action_names.add(action.get('Name'))
176 
177         return matches
178 
179     def match(self, cfn):
180         """Check that stage actions are set up properly."""
181         matches = []
182 
183         resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])
184         for resource in resources:
185             path = resource['Path']
186             properties = resource['Value']
187 
188             s_stages = properties.get_safe('Stages', path)
189             for s_stage_v, s_stage_p in s_stages:
190                 if not isinstance(s_stage_v, list):
191                     self.logger.debug('Stages not list. Should have been caught by generic linting.')
192                     return matches
193 
194                 for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):
195                     action_names = set()
196                     s_actions = l_i_stage.get_safe('Actions', l_i_path)
197                     for s_action_v, s_action_p in s_actions:
198                         if not isinstance(s_action_v, list):
199                             self.logger.debug('Actions not list. Should have been caught by generic linting.')
200                             return matches
201 
202                         for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):
203                             try:
204                                 full_path = path + l_i_a_path
205                                 matches.extend(self.check_names_unique(l_i_a_action, full_path, action_names))
206                                 matches.extend(self.check_version(l_i_a_action, full_path))
207                                 matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', full_path))
208                                 matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', full_path))
209                             except AttributeError as err:
210                                 self.logger.debug('Got AttributeError. Should have been caught by generic linting. '
211                                                   'Ignoring the error here: %s', str(err))
212 
213         return matches
214
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
--- a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
+++ b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
@@ -14,6 +14,7 @@
   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
+import six
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
 
@@ -44,6 +45,12 @@
                     'OutputArtifactRange': (0, 5),
                 }
             },
+            'Build': {
+                'CodeBuild': {
+                    'InputArtifactRange': (1, 5),
+                    'OutputArtifactRange': (0, 5),
+                }
+            },
             'Approval': {
                 'Manual': {
                     'InputArtifactRange': 0,
@@ -167,12 +174,14 @@
         """Check that action names are unique."""
         matches = []
 
-        if action.get('Name') in action_names:
-            message = 'All action names within a stage must be unique. ({name})'.format(
-                name=action.get('Name')
-            )
-            matches.append(RuleMatch(path + ['Name'], message))
-        action_names.add(action.get('Name'))
+        action_name = action.get('Name')
+        if isinstance(action_name, six.string_types):
+            if action.get('Name') in action_names:
+                message = 'All action names within a stage must be unique. ({name})'.format(
+                    name=action.get('Name')
+                )
+                matches.append(RuleMatch(path + ['Name'], message))
+            action_names.add(action.get('Name'))
 
         return matches

{"golden_diff": "diff --git a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n--- a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n+++ b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n@@ -14,6 +14,7 @@\n   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n \"\"\"\n+import six\n from cfnlint import CloudFormationLintRule\n from cfnlint import RuleMatch\n \n@@ -44,6 +45,12 @@\n                     'OutputArtifactRange': (0, 5),\n                 }\n             },\n+            'Build': {\n+                'CodeBuild': {\n+                    'InputArtifactRange': (1, 5),\n+                    'OutputArtifactRange': (0, 5),\n+                }\n+            },\n             'Approval': {\n                 'Manual': {\n                     'InputArtifactRange': 0,\n@@ -167,12 +174,14 @@\n         \"\"\"Check that action names are unique.\"\"\"\n         matches = []\n \n-        if action.get('Name') in action_names:\n-            message = 'All action names within a stage must be unique. ({name})'.format(\n-                name=action.get('Name')\n-            )\n-            matches.append(RuleMatch(path + ['Name'], message))\n-        action_names.add(action.get('Name'))\n+        action_name = action.get('Name')\n+        if isinstance(action_name, six.string_types):\n+            if action.get('Name') in action_names:\n+                message = 'All action names within a stage must be unique. ({name})'.format(\n+                    name=action.get('Name')\n+                )\n+                matches.append(RuleMatch(path + ['Name'], message))\n+            action_names.add(action.get('Name'))\n \n         return matches\n", "issue": "[cfn-lint] E0002:Unknown exception while processing rule E2541: unhashable type: 'dict_node'\n*cfn-lint version: 0.16.0\r\n\r\nI got the error message:\r\n\r\n`[cfn-lint] E0002:Unknown exception while processing rule E2541: unhashable type: 'dict_node'\r\n`\r\n\r\nIf I put a \"!Ref Name\" in the source action Name: see below\r\n      \r\n```\r\nStages:\r\n        - Name: GitHub\r\n          Actions:\r\n          - Name: !Ref GitHubSourceRepo1\r\n            ActionTypeId:\r\n              Category: Source\r\n              Owner: Custom\r\n              Version: 1\r\n              Provider: GitHUBcustom\r\n.\r\n.\r\n.\r\n.\r\n```\r\n\r\nIf I remove  the !Ref, the cfn-lint works fine.\n", "before_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\n\nclass CodepipelineStageActions(CloudFormationLintRule):\n    \"\"\"Check if CodePipeline Stage Actions are set up properly.\"\"\"\n    id = 'E2541'\n    shortdesc = 'CodePipeline Stage Actions'\n    description = 'See if CodePipeline stage actions are set correctly'\n    source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'\n    tags = ['resources', 'codepipeline']\n\n    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'CloudFormation': {\n                    'InputArtifactRange': (0, 10),\n                    'OutputArtifactRange': (0, 1),\n                },\n                'CodeDeploy': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ElasticBeanstalk': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'OpsWorks': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ECS': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n            },\n            'Invoke': {\n                'Lambda': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            }\n        },\n        'ThirdParty': {\n            'Source': {\n                'GitHub': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n        },\n    }\n\n    KEY_MAP = {\n        'InputArtifacts': 'InputArtifactRange',\n        'OutputArtifacts': 'OutputArtifactRange',\n    }\n\n    def check_artifact_counts(self, action, artifact_type, path):\n        \"\"\"Check that artifact counts are within valid ranges.\"\"\"\n        matches = []\n\n        action_type_id = action.get('ActionTypeId')\n        owner = action_type_id.get('Owner')\n        category = action_type_id.get('Category')\n        provider = action_type_id.get('Provider')\n\n        if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):\n            self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')\n            return matches\n\n        constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})\n        if not constraints:\n            return matches\n        artifact_count = len(action.get(artifact_type, []))\n\n        constraint_key = self.KEY_MAP[artifact_type]\n        if isinstance(constraints[constraint_key], tuple):\n            min_, max_ = constraints[constraint_key]\n            if not (min_ <= artifact_count <= max_):\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not in '\n                    'expected range [{a}, {b}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=min_,\n                    b=max_\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n        else:\n            if artifact_count != constraints[constraint_key]:\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not the '\n                    'expected number [{a}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=constraints[constraint_key]\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n\n        return matches\n\n    def check_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        version = action.get('ActionTypeId', {}).get('Version')\n        if isinstance(version, dict):\n            self.logger.debug('Unable to validate version when an object is used.  Skipping')\n        elif version != '1':\n            message = 'For all currently supported action types, the only valid version string is \"1\".'\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Version'],\n                message\n            ))\n        return matches\n\n    def check_names_unique(self, action, path, action_names):\n        \"\"\"Check that action names are unique.\"\"\"\n        matches = []\n\n        if action.get('Name') in action_names:\n            message = 'All action names within a stage must be unique. ({name})'.format(\n                name=action.get('Name')\n            )\n            matches.append(RuleMatch(path + ['Name'], message))\n        action_names.add(action.get('Name'))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check that stage actions are set up properly.\"\"\"\n        matches = []\n\n        resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])\n        for resource in resources:\n            path = resource['Path']\n            properties = resource['Value']\n\n            s_stages = properties.get_safe('Stages', path)\n            for s_stage_v, s_stage_p in s_stages:\n                if not isinstance(s_stage_v, list):\n                    self.logger.debug('Stages not list. Should have been caught by generic linting.')\n                    return matches\n\n                for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):\n                    action_names = set()\n                    s_actions = l_i_stage.get_safe('Actions', l_i_path)\n                    for s_action_v, s_action_p in s_actions:\n                        if not isinstance(s_action_v, list):\n                            self.logger.debug('Actions not list. Should have been caught by generic linting.')\n                            return matches\n\n                        for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):\n                            try:\n                                full_path = path + l_i_a_path\n                                matches.extend(self.check_names_unique(l_i_a_action, full_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, full_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', full_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', full_path))\n                            except AttributeError as err:\n                                self.logger.debug('Got AttributeError. Should have been caught by generic linting. '\n                                                  'Ignoring the error here: %s', str(err))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py"}], "after_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nimport six\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\n\nclass CodepipelineStageActions(CloudFormationLintRule):\n    \"\"\"Check if CodePipeline Stage Actions are set up properly.\"\"\"\n    id = 'E2541'\n    shortdesc = 'CodePipeline Stage Actions'\n    description = 'See if CodePipeline stage actions are set correctly'\n    source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'\n    tags = ['resources', 'codepipeline']\n\n    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            },\n            'Build': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'CloudFormation': {\n                    'InputArtifactRange': (0, 10),\n                    'OutputArtifactRange': (0, 1),\n                },\n                'CodeDeploy': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ElasticBeanstalk': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'OpsWorks': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ECS': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n            },\n            'Invoke': {\n                'Lambda': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            }\n        },\n        'ThirdParty': {\n            'Source': {\n                'GitHub': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n        },\n    }\n\n    KEY_MAP = {\n        'InputArtifacts': 'InputArtifactRange',\n        'OutputArtifacts': 'OutputArtifactRange',\n    }\n\n    def check_artifact_counts(self, action, artifact_type, path):\n        \"\"\"Check that artifact counts are within valid ranges.\"\"\"\n        matches = []\n\n        action_type_id = action.get('ActionTypeId')\n        owner = action_type_id.get('Owner')\n        category = action_type_id.get('Category')\n        provider = action_type_id.get('Provider')\n\n        if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):\n            self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')\n            return matches\n\n        constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})\n        if not constraints:\n            return matches\n        artifact_count = len(action.get(artifact_type, []))\n\n        constraint_key = self.KEY_MAP[artifact_type]\n        if isinstance(constraints[constraint_key], tuple):\n            min_, max_ = constraints[constraint_key]\n            if not (min_ <= artifact_count <= max_):\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not in '\n                    'expected range [{a}, {b}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=min_,\n                    b=max_\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n        else:\n            if artifact_count != constraints[constraint_key]:\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not the '\n                    'expected number [{a}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=constraints[constraint_key]\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n\n        return matches\n\n    def check_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        version = action.get('ActionTypeId', {}).get('Version')\n        if isinstance(version, dict):\n            self.logger.debug('Unable to validate version when an object is used.  Skipping')\n        elif version != '1':\n            message = 'For all currently supported action types, the only valid version string is \"1\".'\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Version'],\n                message\n            ))\n        return matches\n\n    def check_names_unique(self, action, path, action_names):\n        \"\"\"Check that action names are unique.\"\"\"\n        matches = []\n\n        action_name = action.get('Name')\n        if isinstance(action_name, six.string_types):\n            if action.get('Name') in action_names:\n                message = 'All action names within a stage must be unique. ({name})'.format(\n                    name=action.get('Name')\n                )\n                matches.append(RuleMatch(path + ['Name'], message))\n            action_names.add(action.get('Name'))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check that stage actions are set up properly.\"\"\"\n        matches = []\n\n        resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])\n        for resource in resources:\n            path = resource['Path']\n            properties = resource['Value']\n\n            s_stages = properties.get_safe('Stages', path)\n            for s_stage_v, s_stage_p in s_stages:\n                if not isinstance(s_stage_v, list):\n                    self.logger.debug('Stages not list. Should have been caught by generic linting.')\n                    return matches\n\n                for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):\n                    action_names = set()\n                    s_actions = l_i_stage.get_safe('Actions', l_i_path)\n                    for s_action_v, s_action_p in s_actions:\n                        if not isinstance(s_action_v, list):\n                            self.logger.debug('Actions not list. Should have been caught by generic linting.')\n                            return matches\n\n                        for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):\n                            try:\n                                full_path = path + l_i_a_path\n                                matches.extend(self.check_names_unique(l_i_a_action, full_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, full_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', full_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', full_path))\n                            except AttributeError as err:\n                                self.logger.debug('Got AttributeError. Should have been caught by generic linting. '\n                                                  'Ignoring the error here: %s', str(err))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py"}]}