Repository: ansible/ansible-lint

Issue:
Unable to skip rules inside a block
# Issue Type
- Bug report

# Ansible and Ansible Lint details
```
ansible --version
ansible 2.5.5
  config file = /Users/atsvetkov/.ansible.cfg
  configured module search path = [u'/Users/atsvetkov/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Library/Python/2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.10 (default, Oct  6 2017, 22:29:07) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]

ansible-lint --version
ansible-lint 4.1.0
```

# Desired Behaviour
```
  - name: some block name
    block:
      - name: block_1
        shell: yum install vim  # noqa 303
```
ansilbe lint should ignore `yum install ...` warning.

# Actual Behaviour (Bug report only)
ansible lint produces the following output:
`[303] yum used in place of yum module`

But, ` # noqa 303` option will work if we put the same task outside of a block.



Discussion:
I am also seeing this issue. I can work around it by using the tags -> skip_ansible_lint but that is kind of a hammer.

```
tags:
  - skip_ansible_lint
```
This seems to be true for `always:` blocks too.
I'm having the same issue with ansible 2.7.9
This also works but shouldn't be necessary:
```yaml
tags: [skip_ansible_lint]
```

Resolving patch:
diff --git a/lib/ansiblelint/__init__.py b/lib/ansiblelint/__init__.py
--- a/lib/ansiblelint/__init__.py
+++ b/lib/ansiblelint/__init__.py
@@ -77,6 +77,9 @@ def matchtasks(self, file, text):
         if not self.matchtask:
             return matches
 
+        if file['type'] == 'meta':
+            return matches
+
         yaml = ansiblelint.utils.parse_yaml_linenumbers(text, file['path'])
         if not yaml:
             return matches
diff --git a/lib/ansiblelint/utils.py b/lib/ansiblelint/utils.py
--- a/lib/ansiblelint/utils.py
+++ b/lib/ansiblelint/utils.py
@@ -21,6 +21,7 @@
 import glob
 import imp
 import os
+from itertools import product
 
 import six
 from ansible import constants
@@ -599,55 +600,110 @@ def get_first_cmd_arg(task):
 
 
 def append_skipped_rules(pyyaml_data, file_text, file_type):
-    """ Uses ruamel.yaml to parse comments then adds a
-        skipped_rules list to the task (or meta yaml block)
+    """Append 'skipped_rules' to individual tasks or single metadata block.
+
+    For a file, uses 2nd parser (ruamel.yaml) to pull comments out of
+    yaml subsets, check for '# noqa' skipped rules, and append any skips to the
+    original parser (pyyaml) data relied on by remainder of ansible-lint.
+
+    :param pyyaml_data: file text parsed via ansible and pyyaml.
+    :param file_text: raw file text.
+    :param file_type: type of file: tasks, handlers or meta.
+    :returns: original pyyaml_data altered with a 'skipped_rules' list added
+    to individual tasks, or added to the single metadata block.
     """
 
-    PLAYBOOK_TASK_KEYWORDS = [
-        'tasks',
-        'pre_tasks',
-        'post_tasks',
-        'handlers',
-    ]
+    try:
+        yaml_skip = _append_skipped_rules(pyyaml_data, file_text, file_type)
+    except RuntimeError as exc:
+        # Notify user of skip error, do not stop, do not change exit code
+        print('Error trying to append skipped rules: {!r}'.format(exc))
+        return pyyaml_data
+    return yaml_skip
 
+
+def _append_skipped_rules(pyyaml_data, file_text, file_type):
+    # parse file text using 2nd parser library
     yaml = ruamel.yaml.YAML()
     ruamel_data = yaml.load(file_text)
 
+    if file_type == 'meta':
+        pyyaml_data[0]['skipped_rules'] = _get_rule_skips_from_yaml(ruamel_data)
+        return pyyaml_data
+
+    # create list of blocks of tasks or nested tasks
     if file_type in ('tasks', 'handlers'):
-        ruamel_tasks = ruamel_data
-        pyyaml_tasks = pyyaml_data
+        ruamel_task_blocks = ruamel_data
+        pyyaml_task_blocks = pyyaml_data
     elif file_type == 'playbook':
         try:
-            ruamel_tasks = []
-            pyyaml_tasks = []
-            for ruamel_play, pyyaml_play in zip(ruamel_data, pyyaml_data):
-                for key in PLAYBOOK_TASK_KEYWORDS:
-                    ruamel_tasks.extend(ruamel_play.get(key, []))
-                    pyyaml_tasks.extend(pyyaml_play.get(key, []))
+            pyyaml_task_blocks = _get_task_blocks_from_playbook(pyyaml_data)
+            ruamel_task_blocks = _get_task_blocks_from_playbook(ruamel_data)
         except (AttributeError, TypeError):
+            # TODO(awcrosby): running ansible-lint on any .yml file will
+            # assume it is a playbook, check needs to be added higher in the
+            # call stack, and can remove this except
             return pyyaml_data
-    elif file_type == 'meta':
-        if not isinstance(pyyaml_data, list):
-            return pyyaml_data
-        ruamel_tasks = [ruamel_data]
-        pyyaml_tasks = pyyaml_data
     else:
-        return pyyaml_data
+        raise RuntimeError('Unexpected file type: {}'.format(file_type))
 
-    if len(ruamel_tasks) != len(pyyaml_tasks):
-        return pyyaml_data
+    # get tasks from blocks of tasks
+    pyyaml_tasks = _get_tasks_from_blocks(pyyaml_task_blocks)
+    ruamel_tasks = _get_tasks_from_blocks(ruamel_task_blocks)
 
+    # append skipped_rules for each task
     for ruamel_task, pyyaml_task in zip(ruamel_tasks, pyyaml_tasks):
-        skipped_rules = _get_rule_skips_from_task(ruamel_task)
-        if skipped_rules:
-            pyyaml_task['skipped_rules'] = skipped_rules
+        if pyyaml_task.get('name') != ruamel_task.get('name'):
+            raise RuntimeError('Error in matching skip comment to a task')
+        pyyaml_task['skipped_rules'] = _get_rule_skips_from_yaml(ruamel_task)
 
     return pyyaml_data
 
 
-def _get_rule_skips_from_task(task):
+def _get_task_blocks_from_playbook(playbook):
+    """Return parts of playbook that contains tasks, and nested tasks.
+
+    :param playbook: playbook yaml from yaml parser.
+    :returns: list of task dictionaries.
+    """
+    PLAYBOOK_TASK_KEYWORDS = [
+        'tasks',
+        'pre_tasks',
+        'post_tasks',
+        'handlers',
+    ]
+
+    task_blocks = []
+    for play, key in product(playbook, PLAYBOOK_TASK_KEYWORDS):
+        task_blocks.extend(play.get(key, []))
+    return task_blocks
+
+
+def _get_tasks_from_blocks(task_blocks):
+    """Get list of tasks from list made of tasks and nested tasks."""
+    NESTED_TASK_KEYS = [
+        'block',
+        'always',
+        'rescue',
+    ]
+
+    def get_nested_tasks(task):
+        return (
+            subtask
+            for k in NESTED_TASK_KEYS if k in task
+            for subtask in task[k]
+        )
+
+    for task in task_blocks:
+        for sub_task in get_nested_tasks(task):
+            yield sub_task
+        yield task
+
+
+def _get_rule_skips_from_yaml(yaml_input):
+    """Travese yaml for comments with rule skips and return list of rules."""
     def traverse_yaml(obj):
-        task_comment_obj_strs.append(str(obj.ca.items))
+        yaml_comment_obj_strs.append(str(obj.ca.items))
         if isinstance(obj, dict):
             for key, val in obj.items():
                 if isinstance(val, (dict, list)):
@@ -659,11 +715,11 @@ def traverse_yaml(obj):
         else:
             return
 
-    task_comment_obj_strs = []
-    traverse_yaml(task)
+    yaml_comment_obj_strs = []
+    traverse_yaml(yaml_input)
 
     rule_id_list = []
-    for comment_obj_str in task_comment_obj_strs:
+    for comment_obj_str in yaml_comment_obj_strs:
         for line in comment_obj_str.split('\\n'):
             rule_id_list.extend(get_rule_skips_from_line(line))
 


Test patch:
diff --git a/test/TestSkipInsideYaml.py b/test/TestSkipInsideYaml.py
--- a/test/TestSkipInsideYaml.py
+++ b/test/TestSkipInsideYaml.py
@@ -13,36 +13,30 @@
 - name: test 303 (skipped)
   command: git log # noqa 303
   changed_when: False
+'''
 
-- name: test 401 and 501
-  become_user: alice
-  action: git
-- name: test 401 and 501 (skipped)  # noqa 401 501
-  become_user: alice
-  action: git
-- name: test 401 and 501 (skipped via skip_ansible_lint)
-  become_user: alice
-  action: git
-  tags:
-    skip_ansible_lint
-
-- name: test 601
-  debug:
-    msg: testing
-  when: my_var == true
-- name: test 601 (skipped)
-  debug:
-    msg: testing
-  when: my_var == true  # noqa 601
-
-- name: test 303
-  command: git log
-  changed_when: False
-- name: test 303 (skipped via no warn)
-  command: git log
-  args:
-    warn: False
-  changed_when: False
+ROLE_TASKS_WITH_BLOCK = '''
+---
+- name: bad git 1  # noqa 401
+  action: git a=b c=d
+- name: bad git 2
+  action: git a=b c=d
+- name: Block with rescue and always section
+  block:
+    - name: bad git 3  # noqa 401
+      action: git a=b c=d
+    - name: bad git 4
+      action: git a=b c=d
+  rescue:
+    - name: bad git 5  # noqa 401
+      action: git a=b c=d
+    - name: bad git 6
+      action: git a=b c=d
+  always:
+    - name: bad git 7  # noqa 401
+      action: git a=b c=d
+    - name: bad git 8
+      action: git a=b c=d
 '''
 
 PLAYBOOK = '''
@@ -103,7 +97,11 @@ def setUp(self):
 
     def test_role_tasks(self):
         results = self.runner.run_role_tasks_main(ROLE_TASKS)
-        self.assertEqual(5, len(results))
+        self.assertEqual(1, len(results))
+
+    def test_role_tasks_with_block(self):
+        results = self.runner.run_role_tasks_main(ROLE_TASKS_WITH_BLOCK)
+        self.assertEqual(4, len(results))
 
     def test_playbook(self):
         results = self.runner.run_playbook(PLAYBOOK)
diff --git a/test/TestSkipPlaybookItems.py b/test/TestSkipPlaybookItems.py
--- a/test/TestSkipPlaybookItems.py
+++ b/test/TestSkipPlaybookItems.py
@@ -63,6 +63,31 @@
       action: git a=b c=d
 '''
 
+PLAYBOOK_WITH_BLOCK = '''
+- hosts: all
+  tasks:
+  - name: bad git 1  # noqa 401
+    action: git a=b c=d
+  - name: bad git 2
+    action: git a=b c=d
+  - name: Block with rescue and always section
+    block:
+      - name: bad git 3  # noqa 401
+        action: git a=b c=d
+      - name: bad git 4
+        action: git a=b c=d
+    rescue:
+      - name: bad git 5  # noqa 401
+        action: git a=b c=d
+      - name: bad git 6
+        action: git a=b c=d
+    always:
+      - name: bad git 7  # noqa 401
+        action: git a=b c=d
+      - name: bad git 8
+        action: git a=b c=d
+'''
+
 
 class TestSkipPlaybookItems(unittest.TestCase):
     rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
@@ -86,3 +111,7 @@ def test_play_handlers(self):
     def test_two_plays(self):
         results = self.runner.run_playbook(PLAYBOOK_TWO_PLAYS)
         self.assertEqual(2, len(results))
+
+    def test_with_block(self):
+        results = self.runner.run_playbook(PLAYBOOK_WITH_BLOCK)
+        self.assertEqual(4, len(results))