diff --git a/examples/playbooks/tasks/rule-complexity-tasks-fail.yml b/examples/playbooks/tasks/rule-complexity-tasks-fail.yml new file mode 100644 index 0000000000..4f11f31199 --- /dev/null +++ b/examples/playbooks/tasks/rule-complexity-tasks-fail.yml @@ -0,0 +1,26 @@ +--- +# This is a task file (not a playbook) to test complexity[tasks] rule +# It contains 6 tasks, exceeding the maximum of 5 +- name: Task 1 + ansible.builtin.debug: + msg: "This is task 1" + +- name: Task 2 + ansible.builtin.debug: + msg: "This is task 2" + +- name: Task 3 + ansible.builtin.debug: + msg: "This is task 3" + +- name: Task 4 + ansible.builtin.debug: + msg: "This is task 4" + +- name: Task 5 + ansible.builtin.debug: + msg: "This is task 5" + +- name: Task 6 + ansible.builtin.debug: + msg: "This is task 6" diff --git a/src/ansiblelint/rules/complexity.md b/src/ansiblelint/rules/complexity.md index aa25a1e826..cd8983164c 100644 --- a/src/ansiblelint/rules/complexity.md +++ b/src/ansiblelint/rules/complexity.md @@ -6,14 +6,24 @@ suggesting refactoring for better readability and maintainability. ## complexity[tasks] `complexity[tasks]` will be triggered if the total number of tasks inside a file -is above 100. If encountered, you should consider using +is above 100. This counts all tasks across all plays, including tasks nested +within blocks. If encountered, you should consider using [`ansible.builtin.include_tasks`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/include_tasks_module.html) to split your tasks into smaller files. +The threshold can be customized via the `max_tasks` configuration option +(default: 100). + +## complexity[play] + +`complexity[play]` will be triggered if the number of tasks at the play level +(not counting pre_tasks, post_tasks, or handlers) exceeds the configured limit. +This helps ensure that individual plays remain manageable. + ## complexity[nesting] -`complexity[nesting]` will appear when a block contains too many tasks, by -default that number is 20 but it can be changed inside the configuration file by -defining `max_block_depth` value. +`complexity[nesting]` will appear when a block contains too many nested levels, +by default that number is 20 but it can be changed inside the configuration file +by defining `max_block_depth` value. Replace nested block with an include_tasks to make code easier to maintain. Maximum block depth allowed is ... diff --git a/src/ansiblelint/rules/complexity.py b/src/ansiblelint/rules/complexity.py index 820beccef8..2446fe4149 100644 --- a/src/ansiblelint/rules/complexity.py +++ b/src/ansiblelint/rules/complexity.py @@ -2,7 +2,6 @@ from __future__ import annotations -import re import sys from typing import TYPE_CHECKING, Any @@ -16,14 +15,18 @@ class ComplexityRule(AnsibleLintRule): - """Rule for limiting number of tasks inside a file.""" + """Sets maximum complexity to avoid complex plays.""" id = "complexity" - description = "There should be limited tasks executed inside any file" + description = "Checks for complex plays and tasks" + link = "https://ansible.readthedocs.io/projects/lint/rules/complexity/" severity = "MEDIUM" - tags = ["experimental", "idiom"] - version_changed = "6.18.0" - _re_templated_inside = re.compile(r".*\{\{.*\}\}.*\w.*$") + tags = ["experimental"] + + def __init__(self) -> None: + """Initialize the rule.""" + super().__init__() + self._collection: RulesCollection | None = None def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: """Call matchplay for up to no_of_max_tasks inside file and return aggregate results.""" @@ -67,6 +70,44 @@ def matchtask(self, task: Task, file: Lintable | None = None) -> list[MatchError ) return results + def matchtasks(self, file: Lintable) -> list[MatchError]: + """Call matchtask for each task and check total task count.""" + matches: list[MatchError] = [] + + if not isinstance(self._collection, RulesCollection): # pragma: no cover + msg = "Rules cannot be run outside a rule collection." + raise TypeError(msg) + + # Call parent's matchtasks to get all individual task violations + matches = super().matchtasks(file) + + # Only check total task count for task files and handler files + # Playbooks use the complexity[play] check instead + if file.kind in ["handlers", "tasks"]: + # pylint: disable=import-outside-toplevel + from ansiblelint.utils import task_in_list + + task_count = sum( + 1 + for _ in task_in_list( + data=file.data, + file=file, + kind=file.kind, + ) + ) + + # Check if total task count exceeds limit + if task_count > self._collection.options.max_tasks: + matches.append( + self.create_matcherror( + message=f"File contains {task_count} tasks, exceeding the maximum of {self._collection.options.max_tasks}. Consider using `ansible.builtin.include_tasks` to split the tasks into smaller files.", + tag=f"{self.id}[tasks]", + filename=file, + ), + ) + + return matches + def calculate_block_depth(self, task: Task) -> int: """Recursively calculate the block depth of a task.""" if not isinstance(task.position, str): # pragma: no cover @@ -93,6 +134,11 @@ def calculate_block_depth(self, task: Task) -> int: ["complexity[play]", "complexity[nesting]"], id="fail", ), + pytest.param( + "examples/playbooks/tasks/rule-complexity-tasks-fail.yml", + ["complexity[tasks]"], + id="tasks", + ), ), ) def test_complexity(