Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions examples/playbooks/tasks/rule-complexity-tasks-fail.yml
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 14 additions & 4 deletions src/ansiblelint/rules/complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...
58 changes: 52 additions & 6 deletions src/ansiblelint/rules/complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import re
import sys
from typing import TYPE_CHECKING, Any

Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
Loading