Skip to content
Merged
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
11 changes: 10 additions & 1 deletion docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ counterparts:

## Ignoring rules for entire files

Ansible-lint will load skip rules from an `.ansible-lint-ignore` or
Ansible-lint will load ignore rules from an `.ansible-lint-ignore` or
`.config/ansible-lint-ignore.txt` file that should reside adjacent to the config
file. The file format is very simple, containing the filename and the rule to be
ignored. It also supports comments starting with `#`.
Expand All @@ -56,6 +56,15 @@ playbook.yml deprecated-module
The file can also be created by adding `--generate-ignore` to the command line.
Keep in mind that this will override any existing file content.

By default, rules ignored here will raise a non-fatal warning in the
output. If you add `skip` to the line, the test will be skipped
(see `skip_list`) and not raise any warning.

```yaml title=".ansible-lint-ignore"
playbook.yml role-name # raises warning
playbook2.yml role-name skip # no warning
```

## Pre-commit setup

To use Ansible-lint with the [pre-commit] tool, add the following to the
Expand Down
22 changes: 19 additions & 3 deletions src/ansiblelint/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
log_entries,
options,
)
from ansiblelint.loaders import load_ignore_txt
from ansiblelint.loaders import IgnoreRule, IgnoreRuleQualifier, load_ignore_txt
from ansiblelint.output import (
console,
console_stderr,
Expand Down Expand Up @@ -272,6 +272,19 @@ def fix(runtime_options: Options, result: LintResult, rules: RulesCollection) ->
result.matches.pop(idx)


# By default, matches ignored in .ansible-lint-ignore are treated
# as warnings [1]. If the user explicitly adds a skip qualifier
# to the rule, it is treated as skipped here and does not show up
# even as a warning.
# [1] https://github.com/ansible/ansible-lint/issues/3068
def _rule_is_skipped(tag: str, rules: set[IgnoreRule]) -> bool:
for rule in rules:
if tag != rule.rule:
return False
return IgnoreRuleQualifier.SKIP in rule.qualifiers
return False


# pylint: disable=too-many-locals,too-many-statements
def main(argv: list[str] | None = None) -> int:
"""Linter CLI entry point."""
Expand Down Expand Up @@ -376,10 +389,13 @@ def main(argv: list[str] | None = None) -> int:

# Remove skip_list items from the result
result.matches = [m for m in result.matches if m.tag not in app.options.skip_list]
# Mark matches as ignored inside ignore file
# load ignore file
ignore_map = load_ignore_txt(options.ignore_file)
# prune qualified skips from ignore file
result.matches = [m for m in result.matches if not _rule_is_skipped(m.tag, ignore_map[m.filename])]
# others entries are ignored
for match in result.matches:
if match.tag in ignore_map[match.filename]: # pragma: no cover
if match.tag in [i.rule for i in ignore_map[match.filename]]: # pragma: no cover
match.ignored = True
_logger.debug("Ignored: %s", match)

Expand Down
37 changes: 32 additions & 5 deletions src/ansiblelint/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import enum
import logging
import os
from collections import defaultdict
Expand All @@ -23,11 +24,21 @@

class IgnoreFile(NamedTuple):
"""IgnoreFile n."""

default: str
alternative: str


class IgnoreRuleQualifier(enum.Enum):
"""Extra flags for ignored rules."""
SKIP = "Force skip, not warning"


class IgnoreRule(NamedTuple):
"""Ignored rule."""
rule: str
qualifiers: frozenset[IgnoreRuleQualifier]


IGNORE_FILE = IgnoreFile(".ansible-lint-ignore", ".config/ansible-lint-ignore.txt")

yaml_load = partial(yaml.load, Loader=FullLoader)
Expand All @@ -41,7 +52,19 @@ def yaml_from_file(filepath: str | Path) -> Any:
return yaml_load(content)


def load_ignore_txt(filepath: Path | None = None) -> dict[str, set[str]]:
def get_ignore_rule(rule: str, qualifiers: str) -> IgnoreRule:
"""Validate qualifiers and return an IgnoreRule."""
s = set()
if qualifiers:
for q in qualifiers.split(","):
if q == "skip":
s.add(IgnoreRuleQualifier.SKIP)
else:
raise ValueError
return IgnoreRule(rule, frozenset(s))


def load_ignore_txt(filepath: Path | None = None) -> dict[str, set[IgnoreRule]]:
"""Return a list of rules to ignore."""
result = defaultdict(set)

Expand All @@ -64,17 +87,21 @@ def load_ignore_txt(filepath: Path | None = None) -> dict[str, set[str]]:
entry = line.split("#")[0].rstrip()
if entry:
try:
path, rule = entry.split()
fields = entry.split()
path = fields[0]
rule = fields[1]
qualifiers = fields[2] if len(fields) == 3 else ""
result[path].add(get_ignore_rule(rule, qualifiers))
except ValueError as exc: # pragma: no cover
msg = f"Unable to parse line '{line}' from {ignore_file} file."
raise RuntimeError(msg) from exc
result[path].add(rule)

return result


__all__ = [
"IGNORE_FILE",
"IgnoreRule",
"IgnoreRuleQualifier",
"YAMLError",
"load_ignore_txt",
"yaml_from_file",
Expand Down
46 changes: 38 additions & 8 deletions test/test_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
from pathlib import Path
from textwrap import dedent

from ansiblelint.loaders import IGNORE_FILE, load_ignore_txt
import pytest

from ansiblelint.loaders import (
IGNORE_FILE,
IgnoreRule,
IgnoreRuleQualifier,
load_ignore_txt,
)


def test_load_ignore_txt_default_empty() -> None:
Expand Down Expand Up @@ -35,6 +42,7 @@ def test_load_ignore_txt_default_success() -> None:
# See https://ansible.readthedocs.io/projects/lint/configuring/#ignoring-rules-for-entire-files
playbook2.yml package-latest # comment
playbook2.yml foo-bar
playbook2.yml another-role skip # rule with qualifier
""",
),
)
Expand All @@ -47,7 +55,10 @@ def test_load_ignore_txt_default_success() -> None:
finally:
os.chdir(cwd)

assert result == {"playbook2.yml": {"package-latest", "foo-bar"}}
assert result == {"playbook2.yml":
{IgnoreRule("package-latest", frozenset()),
IgnoreRule("foo-bar", frozenset()),
IgnoreRule("another-role", frozenset([IgnoreRuleQualifier.SKIP]))}}


def test_load_ignore_txt_default_success_alternative() -> None:
Expand Down Expand Up @@ -76,8 +87,8 @@ def test_load_ignore_txt_default_success_alternative() -> None:
os.chdir(cwd)

assert result == {
"playbook.yml": {"more-foo", "foo-bar"},
"tasks/main.yml": {"more-bar"},
"playbook.yml": {IgnoreRule("more-foo", frozenset()), IgnoreRule("foo-bar", frozenset())},
"tasks/main.yml": {IgnoreRule("more-bar", frozenset())},
}


Expand Down Expand Up @@ -108,10 +119,10 @@ def test_load_ignore_txt_custom_success() -> None:
os.chdir(cwd)

assert result == {
"playbook.yml": {"hector"},
"roles/eduardo/tasks/main.yml": {"lalo"},
"roles/guzman/tasks/main.yml": {"lalo"},
"vars/main.yml": {"tuco"},
"playbook.yml": {IgnoreRule("hector", frozenset())},
"roles/eduardo/tasks/main.yml": {IgnoreRule("lalo", frozenset())},
"roles/guzman/tasks/main.yml": {IgnoreRule("lalo", frozenset())},
"vars/main.yml": {IgnoreRule("tuco", frozenset())},
}


Expand All @@ -120,3 +131,22 @@ def test_load_ignore_txt_custom_fail() -> None:
result = load_ignore_txt(Path(str(uuid.uuid4())))

assert not result


def test_load_ignore_txt_invalid_tags(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test load_ignore_txt with an existing ignore-file in the default location."""
with tempfile.TemporaryDirectory() as temporary_directory:
ignore_file = Path(temporary_directory) / IGNORE_FILE.default

with ignore_file.open("w", encoding="utf-8") as _ignore_file:
_ignore_file.write(
dedent(
"""
playbook2.yml package-latest invalid-tag
""",
),
)

monkeypatch.chdir(temporary_directory)
with pytest.raises(RuntimeError, match="Unable to parse line"):
load_ignore_txt()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ set_env =
PIP_CONSTRAINT = {tox_root}/.config/constraints.txt
PIP_DISABLE_PIP_VERSION_CHECK = 1
PRE_COMMIT_COLOR = always
PYTEST_REQPASS = 911
PYTEST_REQPASS = 912
UV_CONSTRAINT = {tox_root}/.config/constraints.txt
deps, devel, hook, lint, pkg, pre, py310, schemas: PIP_CONSTRAINT = /dev/null
deps, devel, hook, lint, pkg, pre, py310, schemas: UV_CONSTRAINT = /dev/null
Expand Down