Skip to content

Commit

Permalink
Allow rules to expose all tags they can produce
Browse files Browse the repository at this point in the history
Related: #2935
  • Loading branch information
ssbarnea committed May 19, 2023
1 parent a782eaf commit 2dfba4b
Show file tree
Hide file tree
Showing 19 changed files with 182 additions and 22 deletions.
8 changes: 8 additions & 0 deletions src/ansiblelint/_internal/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ def __repr__(self) -> str:
"""Return a AnsibleLintRule instance representation."""
return self.id + ": " + self.shortdesc

@classmethod
def ids(cls) -> dict[str, str]:
"""Return a dictionary ids and their messages.
This is used by the ``--list-tags`` option to ansible-lint.
"""
return getattr(cls, "_ids", {cls.id: cls.shortdesc})


# pylint: enable=unused-argument

Expand Down
10 changes: 8 additions & 2 deletions src/ansiblelint/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,23 +503,29 @@ def list_tags(self) -> str:
"metadata": "Invalid metadata, likely related to galaxy, collections or roles",
"opt-in": "Rules that are not used unless manually added to `enable_list`",
"security": "Rules related o potentially security issues, like exposing credentials",
"syntax": "Related to wrong or deprecated syntax",
"unpredictability": "Warn about code that might not work in a predictable way",
"unskippable": "Indicate a fatal error that cannot be ignored or disabled",
"yaml": "External linter which will also produce its own rule codes",
}

tags = defaultdict(list)
for rule in self.rules:
# Fail early if a rule does not have any of our required tags
if not set(rule.tags).intersection(tag_desc.keys()):
msg = f"Rule {rule} does not have any of the required tags: {', '.join(tag_desc.keys())}"
raise RuntimeError(msg)
for tag in rule.tags:
tags[tag].append(rule.id)
for id_ in rule.ids():
tags[tag].append(id_)
result = "# List of tags and rules they cover\n"
for tag in sorted(tags):
desc = tag_desc.get(tag, None)
if desc:
result += f"{tag}: # {desc}\n"
else:
result += f"{tag}:\n"
for name in tags[tag]:
for name in sorted(tags[tag]):
result += f" - {name}\n"
return result

Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ class ArgsRule(AnsibleLintRule):
tags = ["syntax", "experimental"]
version_added = "v6.10.0"
module_aliases: dict[str, str] = {"block/always/rescue": "block/always/rescue"}
_ids = {
"args[module]": description,
}

def matchtask( # noqa: C901
self,
Expand Down
65 changes: 64 additions & 1 deletion src/ansiblelint/rules/fqcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,69 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin):
tags = ["formatting"]
version_added = "v6.8.0"
module_aliases: dict[str, str] = {"block/always/rescue": "block/always/rescue"}
# List of special variables that should be treated as read-only. This list
# does not include connection variables, which we expect users to tune in
# specific cases.
# https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html
read_only_names = {
"ansible_check_mode",
"ansible_collection_name",
"ansible_config_file",
"ansible_dependent_role_names",
"ansible_diff_mode",
"ansible_forks",
"ansible_index_var",
"ansible_inventory_sources",
"ansible_limit",
"ansible_local", # special fact
"ansible_loop",
"ansible_loop_var",
"ansible_parent_role_names",
"ansible_parent_role_paths",
"ansible_play_batch",
"ansible_play_hosts",
"ansible_play_hosts_all",
"ansible_play_name",
"ansible_play_role_names",
"ansible_playbook_python",
"ansible_role_name",
"ansible_role_names",
"ansible_run_tags",
"ansible_search_path",
"ansible_skip_tags",
"ansible_verbosity",
"ansible_version",
"group_names",
"groups",
"hostvars",
"inventory_dir",
"inventory_file",
"inventory_hostname",
"inventory_hostname_short",
"omit",
"play_hosts",
"playbook_dir",
"role_name",
"role_names",
"role_path",
}

# These special variables are used by Ansible but we allow users to set
# them as they might need it in certain cases.
allowed_special_names = {
"ansible_facts",
"ansible_become_user",
"ansible_connection",
"ansible_host",
"ansible_python_interpreter",
"ansible_user",
"ansible_remote_tmp", # no included in docs
}
_ids = {
"fqcn[action-core]": "Use FQCN for builtin module actions",
"fqcn[action]": "Use FQCN for module actions",
"fqcn[canonical]": "You should use canonical module name",
}

def matchtask(
self,
Expand Down Expand Up @@ -188,7 +251,7 @@ def transform(
lintable: Lintable,
data: CommentedMap | CommentedSeq | str,
) -> None:
if match.tag in {"fqcn[action-core]", "fqcn[action]", "fqcn[canonical]"}:
if match.tag in self.ids():
target_task = self.seek(match.yaml_path, data)
# Unfortunately, a lot of data about Ansible content gets lost here, you only get a simple dict.
# For now, just parse the error messages for the data about action names etc. and fix this later.
Expand Down
7 changes: 7 additions & 0 deletions src/ansiblelint/rules/galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class GalaxyRule(AnsibleLintRule):
severity = "MEDIUM"
tags = ["metadata"]
version_added = "v6.11.0 (last update)"
_ids = {
"galaxy[tags]": "galaxy.yaml must have one of the required tags",
"galaxy[no-changelog]": "No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.",
"galaxy[version-missing]": "galaxy.yaml should have version tag.",
"galaxy[version-incorrect]": "collection version should be greater than or equal to 1.0.0",
"galaxy[no-runtime]": "meta/runtime.yml file not found.",
}

def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
"""Return matches found for a specific play (entry in playbook)."""
Expand Down
23 changes: 6 additions & 17 deletions src/ansiblelint/rules/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,7 @@
Token = namedtuple("Token", "lineno token_type value")

ignored_re = re.compile(
"|".join( # noqa: FLY002
[
r"^Object of type method is not JSON serializable",
r"^Unexpected templating type error occurred on",
r"^obj must be a list of dicts or a nested dict$",
r"^the template file (.*) could not be found for the lookup$",
r"could not locate file in lookup",
r"unable to locate collection",
r"^Error in (.*)is undefined$",
r"^Mandatory variable (.*) not defined.$",
r"is undefined",
r"Unrecognized type <<class 'ansible.template.AnsibleUndefined'>> for (.*) filter <value>$",
# https://github.com/ansible/ansible-lint/issues/3155
r"^The '(.*)' test expects a dictionary$",
],
),
"^Object of type method is not JSON serializable|^Unexpected templating type error occurred on|^obj must be a list of dicts or a nested dict$|^the template file (.*) could not be found for the lookup$|could not locate file in lookup|unable to locate collection|^Error in (.*)is undefined$|^Mandatory variable (.*) not defined.$|is undefined|Unrecognized type <<class 'ansible.template.AnsibleUndefined'>> for (.*) filter <value>$|^The '(.*)' test expects a dictionary$",
flags=re.MULTILINE | re.DOTALL,
)

Expand All @@ -70,6 +55,10 @@ class JinjaRule(AnsibleLintRule):
"invalid": "Syntax error in jinja2 template: {value}",
"spacing": "Jinja2 spacing could be improved: {value} -> {reformatted}",
}
_ids = {
"jinja[invalid]": "Invalid jinja2 syntax",
"jinja[spacing]": "Jinja2 spacing could be improved",
}

def _msg(self, tag: str, value: str, reformatted: str) -> str:
"""Generate error message."""
Expand Down Expand Up @@ -246,7 +235,7 @@ def unlex(self, tokens: list[Token]) -> str:
return result

# pylint: disable=too-many-branches,too-many-statements,too-many-locals
def check_whitespace( # noqa: max-complexity: 13
def check_whitespace( # noqa: C901
self,
text: str,
key: str,
Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/key_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class KeyOrderRule(AnsibleLintRule):
tags = ["formatting"]
version_added = "v6.6.2"
needs_raw_task = True
_ids = {
"key-order[task]": "You can improve the task key order",
}

def matchtask(
self,
Expand Down
4 changes: 4 additions & 0 deletions src/ansiblelint/rules/latest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class LatestRule(AnsibleLintRule):
severity = "MEDIUM"
tags = ["idempotency"]
version_added = "v6.5.2"
_ids = {
"latest[git]": "Use a commit hash or tag instead of 'latest' for git",
"latest[hg]": "Use a commit hash or tag instead of 'latest' for hg",
}

def matchtask(
self,
Expand Down
4 changes: 4 additions & 0 deletions src/ansiblelint/rules/loop_var_prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class RoleLoopVarPrefix(AnsibleLintRule):
tags = ["idiom"]
prefix = re.compile("")
severity = "MEDIUM"
_ids = {
"loop-var-prefix[wrong]": "Loop variable name does not match regex.",
"loop-var-prefix[missing]": "Replace unsafe implicit `item` loop variable.",
}

def matchtask(
self,
Expand Down
4 changes: 4 additions & 0 deletions src/ansiblelint/rules/meta_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class CheckRequiresAnsibleVersion(AnsibleLintRule):
# Refer to https://access.redhat.com/support/policy/updates/ansible-automation-platform
# Also add devel to this list
supported_ansible = ["2.9.10", "2.11.", "2.12.", "2.13.", "2.14.", "2.15.", "2.16."]
_ids = {
"meta-runtime[unsupported-version]": "requires_ansible key must be set to a supported version.",
"meta-runtime[invalid-version]": "'requires_ansible' is not a valid requirement specification",
}

def matchyaml(self, file: Lintable) -> list[MatchError]:
"""Find violations inside meta files.
Expand Down
7 changes: 7 additions & 0 deletions src/ansiblelint/rules/name.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class NameRule(AnsibleLintRule, TransformMixin):
tags = ["idiom"]
version_added = "v6.9.1 (last update)"
_re_templated_inside = re.compile(r".*\{\{.*\}\}.*\w.*$")
_ids = {
"name[play]": "All plays should be named.",
"name[missing]": "All tasks should be named.",
"name[prefix]": "Task name should start with a prefix.",
"name[casing]": "All names should start with an uppercase letter.",
"name[template]": "Jinja templates should only be at the end of 'name'",
}

def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
"""Return matches found for a specific play (entry in playbook)."""
Expand Down
4 changes: 4 additions & 0 deletions src/ansiblelint/rules/no_free_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class NoFreeFormRule(AnsibleLintRule):
cmd_shell_re = re.compile(
r"(chdir|creates|executable|removes|stdin|stdin_add_newline|warn)=",
)
_ids = {
"no-free-form[raw]": "Avoid embedding `executable=` inside raw calls, use explicit args dictionary instead.",
"no-free-form[raw-non-string]": "Passing a non string value to `raw` module is neither documented or supported.",
}

def matchtask(
self,
Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/role_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class RoleNames(AnsibleLintRule):
severity = "HIGH"
tags = ["deprecations", "metadata"]
version_added = "v6.8.5"
_ids = {
"role-name[path]": "Avoid using paths when importing roles.",
}

def matchtask(
self,
Expand Down
4 changes: 4 additions & 0 deletions src/ansiblelint/rules/run_once.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class RunOnce(AnsibleLintRule):

tags = ["idiom"]
severity = "MEDIUM"
_ids = {
"run-once[task]": "Using run_once may behave differently if strategy is set to free.",
"run-once[play]": "Play uses strategy: free",
}

def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
"""Return matches found for a specific playbook."""
Expand Down
6 changes: 5 additions & 1 deletion src/ansiblelint/rules/sanity.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class CheckSanityIgnoreFiles(AnsibleLintRule):
"Identifies non-allowed entries in the `tests/sanity/ignore*.txt files."
)
severity = "MEDIUM"
tags = []
tags = ["idiom"]
version_added = "v6.14.0"

# Partner Engineering defines this list. Please contact PE for changes.
Expand All @@ -48,6 +48,10 @@ class CheckSanityIgnoreFiles(AnsibleLintRule):
"compile-3.5",
"compile-3.5!skip",
]
_ids = {
"sanity[cannot-ignore]": "Ignore file contains ... at line ..., which is not a permitted ignore.",
"sanity[bad-ignore]": "Ignore file entry at ... is formatted incorrectly. Please review.",
}

def matchyaml(self, file: Lintable) -> list[MatchError]:
"""Evaluate sanity ignore lists for disallowed ignores.
Expand Down
17 changes: 17 additions & 0 deletions src/ansiblelint/rules/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ class ValidateSchemaRule(AnsibleLintRule):
severity = "VERY_HIGH"
tags = ["core"]
version_added = "v6.1.0"
_ids = {
"schema[ansible-lint-config]": "",
"schema[ansible-navigator-config]": "",
"schema[changelog]": "",
"schema[execution-environment]": "",
"schema[galaxy]": "",
"schema[inventory]": "",
"schema[meta]": "",
"schema[meta-runtime]": "",
"schema[molecule]": "",
"schema[playbook]": "",
"schema[requirements]": "",
"schema[role-arg-spec]": "",
"schema[rulebook]": "",
"schema[tasks]": "",
"schema[vars]": "",
}

def matchtask(
self,
Expand Down
5 changes: 5 additions & 0 deletions src/ansiblelint/rules/var_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ class VariableNamingRule(AnsibleLintRule):
"ansible_user",
"ansible_remote_tmp", # no included in docs
}
_ids = {
"var-naming[no-reserved]": "Variables names must not be Ansible reserved names.",
"var-naming[no-jinja]": "Variables names must not contain jinja2 templating.",
"var-naming[pattern]": f"Variables names should match {re_pattern_str} regex.",
}

# pylint: disable=too-many-return-statements
def get_var_naming_matcherror(
Expand Down
25 changes: 25 additions & 0 deletions src/ansiblelint/rules/yaml_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ class YamllintRule(AnsibleLintRule):
link = "https://yamllint.readthedocs.io/en/stable/rules.html"
# ensure this rule runs before most of other common rules
_order = 1
_ids = {
"yaml[anchors]": "",
"yaml[braces]": "",
"yaml[brackets]": "",
"yaml[colons]": "",
"yaml[commas]": "",
"yaml[comments-indentation]": "",
"yaml[comments]": "",
"yaml[document-end]": "",
"yaml[document-start]": "",
"yaml[empty-lines]": "",
"yaml[empty-values]": "",
"yaml[float-values]": "",
"yaml[hyphens]": "",
"yaml[indentation]": "",
"yaml[key-duplicates]": "",
"yaml[key-ordering]": "",
"yaml[line-length]": "",
"yaml[new-line-at-end-of-file]": "",
"yaml[new-lines]": "",
"yaml[octal-values]": "",
"yaml[quoted-strings]": "",
"yaml[trailing-spaces]": "",
"yaml[truthy]": "",
}

def matchyaml(self, file: Lintable) -> list[MatchError]:
"""Return matches found for a specific YAML text."""
Expand Down
2 changes: 1 addition & 1 deletion src/ansiblelint/schemas/__store__.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/inventory.json"
},
"meta": {
"etag": "24aa044eddbf2fc92e31775bcc625fd8e7689cb14542ac59c0e3b94d9a9b163a",
"etag": "7b5ac2250a4ae70ef657cd9906e6c13f4941daba71724e3342b7fa7e7239a334",
"url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/meta.json"
},
"meta-runtime": {
Expand Down

0 comments on commit 2dfba4b

Please sign in to comment.