Files
computer-playbook/tests/lint/filesystem/python/test_no_redundant_bool_patterns.py
T
kevinveenbirkenbachandClaude Opus 4.7 9a578952c3 feat(env): SPOT env-pipeline + INFINITO_* namespace
Replace 8 bash env modules with a single Python generator
(cli.meta.env) that materialises env/static.env + runtime context
into a gitignored .env. Each variable lives in its own handler
under utils/env/handlers/; defaults carry inline one-line `# ...`
comments.

Rename to the INFINITO_* schema:
- workflow inputs: APPS, SERVICES_DISABLED, CMD, DEBUG, WHITELIST,
  BUNDLES, FULL_CYCLE, VARIANT, RUN_FLAGS, LIMIT_HOST, PARALLEL
- inventory: INVENTORY_DIR / INVENTORY_FILE / HOST_VARS_FILE,
  PW_FILE -> INVENTORY_PASSWORD_FILE
- venv: VENV_BASE / VENV_FALLBACK / VENV_NAME
- new SPOT defaults: COMPILE_SILENCE, CONTAINER_NO_CA,
  WAIT_HEALTH_TIMEOUT_S, PACKAGE_CACHE_FRONTEND_INIT_IMAGE,
  promoted IMAGE_TAG from GHA-only to general passthrough

compose.yml hardening:
- inline \${VAR:-default} removed; strict \${VAR:?...} everywhere
- new integration lint blocks regressions

Tooling:
- scripts/install/all.sh owns the stamp-file install logic
  (Makefile target delegates)
- scripts/meta/env/load.sh is the single shell entry point;
  8 bash modules deleted
- new shell lint: no INFINITO_* defaults outside env/static.env

Tests:
- 93 new unit tests for utils/env/{builder,parser,runtime,writer}
- restructure tests/lint into repository/ (file-content rules) vs
  filesystem/{shell,python,env}/ (filesystem-shape rules)
- drift + smoke integration tests against the generator

Docs:
- variables.md is now a pointer; per-variable docs live next to
  the key in env/static.env / env/ci.env
- compose.yml.md drops the Default column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:40:34 +02:00

182 lines
6.8 KiB
Python

"""Lint guard: redundant boolean expressions in Jinja2 / Ansible YAML.
Two related anti-patterns are flagged here. Each is independently
suppressible via its own marker so a legitimate exception can opt out
of one without disabling the other.
1. ``redundant-bool-ternary`` — ``<bool-expr> | ternary('true','false')``
for any boolean-producing left side: the ``bool`` filter, an ``in``
/ ``not in`` / ``is`` / ``is not`` / comparison operator, a Jinja
``is defined`` / ``is none`` test, etc. The ``ternary`` is a no-op
on a value that is already a boolean — its only effect is to swap
``True`` / ``False`` for the lowercase string tokens
``'true'`` / ``'false'``, and every consumer the project actually
feeds these to (PHP ``filter_var(..., FILTER_VALIDATE_BOOLEAN)``,
JS ``String(x).toLowerCase() === "true"``, dotenv-style flags,
Ansible ``bool``-comparing conditionals) accepts ``True`` / ``False``
just as well. Convention::
{{ FOO | bool }}
{{ 'web-app-lam' in group_names }}
{{ (A == B) }}
The detector keys on the literal ``'true'`` / ``'false'`` payload
inside ``ternary(…)``. Real string conversions that happen to use
``ternary`` use other labels (``'yes'`` / ``'no'``, ``'1'`` / ``'0'``,
…) and are NOT matched. ``... | bool | string | lower`` is also NOT
matched — it lower-cases the output to the literal token
``true`` / ``false``, which IS sometimes required by Rails / Elixir
consumers.
2. ``redundant-bool-comparison`` — ``... | bool == True/False`` and the
lower-case / quoted variants (``== true``, ``== "false"``, …).
Comparing a boolean against a boolean literal is a tautology that
hides intent. Convention::
when: FOO | bool # truthy branch
when: not (FOO | bool) # falsy branch
Per-line opt-out::
# nocheck: redundant-bool-ternary
{# nocheck: redundant-bool-comparison #}
The marker grammar is documented at
``docs/contributing/actions/testing/suppression.md``.
"""
from __future__ import annotations
import re
import unittest
from pathlib import Path
from utils.annotations.suppress import suppressed_line_numbers
from utils.cache.files import PROJECT_ROOT, iter_project_files, read_text
# `... | ternary('true','false')` — anywhere. The literal `'true'` /
# `'false'` payload is the signal that the upstream value is already a
# boolean (or boolean-equivalent) and the ternary is converting it back
# into the same truth value as a string.
_PATTERN_TERNARY: re.Pattern[str] = re.compile(
r"""\|\s*ternary\(\s*['"]true['"]\s*,\s*['"]false['"]\s*\)""",
)
# `| bool == ` followed by a bool literal (case-insensitive, optional quotes).
_PATTERN_COMPARISON: re.Pattern[str] = re.compile(
r"""\|\s*bool\s*==\s*(?:['"]?(?:true|false)['"]?)""",
re.IGNORECASE,
)
_SCAN_EXTENSIONS: tuple[str, ...] = (".j2", ".yml", ".yaml")
def _scan_paths() -> list[Path]:
"""Iterate every production template / YAML file via the shared
file-walk cache (tests dir excluded)."""
out: list[Path] = []
for s in iter_project_files(extensions=_SCAN_EXTENSIONS, exclude_tests=True):
p = Path(s)
try:
p.relative_to(PROJECT_ROOT)
except ValueError:
continue
out.append(p)
return sorted(out)
def _file_offenders(path: Path, pattern: re.Pattern[str], rule: str) -> list[str]:
"""Return ``[]`` if the file is clean, or a list of human-readable
``line N: <snippet>`` strings for each match of ``pattern`` on a
line not annotated with ``noqa: <rule>``.
"""
try:
src = read_text(str(path))
except (OSError, UnicodeDecodeError):
return []
lines = src.splitlines()
noqa_lines = suppressed_line_numbers(lines, rule)
offenders: list[str] = []
for idx, line in enumerate(lines, start=1):
if idx in noqa_lines:
continue
if pattern.search(line) is None:
continue
offenders.append(f"line {idx}: {line.strip()}")
return offenders
def _collect(pattern: re.Pattern[str], rule: str) -> dict[Path, list[str]]:
out: dict[Path, list[str]] = {}
for path in _scan_paths():
issues = _file_offenders(path, pattern, rule)
if issues:
out[path] = issues
return out
def _format_failure(
headline: str,
offenders: dict[Path, list[str]],
fix: str,
) -> str:
rel = lambda p: p.relative_to(PROJECT_ROOT) # noqa: E731
lines = [headline]
for path, issues in sorted(offenders.items()):
lines.append(f" - {rel(path)}:")
lines.extend(f" * {issue}" for issue in issues)
lines.append("")
lines.append(fix)
return "\n".join(lines)
class TestNoRedundantBoolPatterns(unittest.TestCase):
"""Reject ``| bool | ternary(…)`` and ``| bool == <bool-literal>``."""
def test_no_redundant_bool_ternary(self) -> None:
offenders = _collect(_PATTERN_TERNARY, "redundant-bool-ternary")
if not offenders:
return
self.fail(
_format_failure(
f"{len(offenders)} file(s) end a boolean expression with "
f"`| ternary('true','false')`. That ternary is a no-op on "
f"a value that is already a boolean — its only effect is "
f"to swap `True`/`False` for the lowercase string tokens "
f"`'true'`/`'false'`, which every consumer the project "
f"actually feeds them to accepts unchanged:",
offenders,
"Fix: drop the `| ternary('true','false')` segment. The "
"preceding expression already evaluates to the truth value "
"you want — `FOO | bool`, `'web-app-X' in group_names`, "
"`(A == B)`, `FOO is defined`, etc. If the consumer "
"truly requires the literal lowercase tokens 'true'/'false', "
"use `... | bool | string | lower` and annotate the line "
"with `noqa: redundant-bool-ternary` (with a one-line "
"comment explaining why).",
)
)
def test_no_redundant_bool_comparison(self) -> None:
offenders = _collect(_PATTERN_COMPARISON, "redundant-bool-comparison")
if not offenders:
return
self.fail(
_format_failure(
f"{len(offenders)} file(s) compare `... | bool` against a "
f"bool literal. The comparison is a no-op — `... | bool` "
f"already produces the boolean you are testing for:",
offenders,
"Fix: replace `FOO | bool == True` with `FOO | bool` and "
"`FOO | bool == False` with `not (FOO | bool)`. If a real "
"tri-state guard is intended, annotate the line with "
"`noqa: redundant-bool-comparison` and document why.",
)
)
if __name__ == "__main__": # pragma: no cover
unittest.main()