Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4e66803
Add match-dispatch and assert-message pylint checkers
leynos Jul 23, 2026
c53003b
Add constant-comparison chain checker
leynos Jul 23, 2026
7f7a1eb
Document the provided pylint checkers
leynos Jul 23, 2026
994184d
Reformat AGENTS.md and regenerate typos configuration
leynos Jul 23, 2026
867735f
Add wrapper, re-export, and suppression checkers
leynos Jul 23, 2026
ac52627
Document the wrapper, re-export, and suppression checkers
leynos Jul 23, 2026
6c507fd
Add ambrleaks scanner for unredacted snapshot values
leynos Jul 23, 2026
5a670c0
Add snapshot assertion checker
leynos Jul 23, 2026
6cb49e7
Rewrite README in the df12 house style
leynos Jul 23, 2026
b410bef
Add trivial-alias-wrapper checker
leynos Jul 23, 2026
7b24203
Extract pure chain kernels and model-check them with CrossHair
leynos Jul 23, 2026
fdb09ef
Detect negative number literals in constant chains
leynos Jul 23, 2026
77edd55
Add Hypothesis property tests for checker kernels
leynos Jul 23, 2026
215606d
Add end-to-end tests under the PyPy pylint shim
leynos Jul 23, 2026
239797d
Tighten pass-through and import resolution in wrapper checkers
leynos Jul 23, 2026
ced4981
Accept "astroid" in the spelling gate
leynos Jul 23, 2026
36bad81
Preserve multiplicity in the ambrleaks baseline
leynos Jul 23, 2026
a2d755b
Use structural pattern matching in the chain checkers
leynos Jul 23, 2026
0871a6a
Share the import-binding predicate between checkers
leynos Jul 23, 2026
c3ac507
Detect uppercase suppression pragmas without explanations
leynos Jul 23, 2026
744a6d9
Tighten kernel and entropy properties to exact outputs
leynos Jul 23, 2026
ba8bcd4
Harden ambrleaks configuration, paths, and coverage
leynos Jul 23, 2026
80dc8d6
Correct checker count and polish documentation
leynos Jul 23, 2026
0cf9f53
Cover the error paths added by the review round
leynos Jul 23, 2026
09ea480
Document the shared snapshot-writer fixture properly
leynos Jul 24, 2026
dcffd70
Use a raw docstring for the fixture usage example
leynos Jul 24, 2026
24caf39
Address the documentation, boundary, and path-rule review round
leynos Jul 24, 2026
cd3984c
Split ambrleaks I/O boundaries and enforce CrossHair confirmation
Jul 25, 2026
1953cd7
Add prefer-type-statement and redundant-future-annotations checkers
Jul 25, 2026
0303769
Deduplicate snapshot-assert test boilerplate flagged by CodeScene
Jul 25, 2026
74273e2
Extract direct-substring-probe traversal from visit_functiondef
Jul 25, 2026
2d06737
Parametrize the absolute-path snapshot scanner regression tests
Jul 25, 2026
6670fce
Share wrapper-test boilerplate via a private base test case
Jul 25, 2026
c0299a4
Share re-export test boilerplate via an instance helper
Jul 25, 2026
41d39bc
Share constant-chain test boilerplate via instance helpers
Jul 25, 2026
cb0558a
Share match-dispatch test boilerplate via instance helpers
Jul 25, 2026
481253c
Flatten _is_passthrough_call via a _forwarded_names helper
Jul 25, 2026
7841dc1
Flatten _imported_origin via _assignment_import_origin helper
Jul 25, 2026
8201d2c
Share type-alias test boilerplate via instance helpers
Jul 25, 2026
4ebd3c9
Split _validate_string_list out of _validate_allowlist
Jul 26, 2026
b533aa1
Decode import forms in per-kind helpers, flattening the dispatcher
Jul 26, 2026
257d9fb
Action inline-review findings: mask CLI values, match/case, doc/test …
Jul 26, 2026
e918bb9
Shorten _validate_allowlist docstring to a single-line summary
Jul 26, 2026
e16dc27
Document ambrleaks' deliberate lack of telemetry
Jul 26, 2026
f17eab4
Restore coverage for branches exposed by recent refactors
Jul 26, 2026
431db6f
Shorten import-binding helper docstrings to single-line summaries
Jul 26, 2026
f8b4183
Add boundary logging to ambrleaks and split out its config module
Jul 26, 2026
64fbbe5
Stream ambrleaks path discovery and finding collection
Jul 26, 2026
f2837cd
Fix guide punctuation and document ambrleaks exit status 2
Jul 26, 2026
7c5dc1c
Stream ambrleaks discovery, baseline suppression, and baseline writing
Jul 26, 2026
c0b7659
Document discover() in NumPy style and trim helper/guide prose
Jul 26, 2026
a2fd0e4
Condense _print_findings docstring to a single-line summary
Jul 26, 2026
1724839
Cover write_baseline's failure-cleanup path
Jul 26, 2026
737d47d
Condense _count_stream docstring and add _source example
Jul 26, 2026
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
9 changes: 3 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@
- Record the decision in architecture, design, or developers-guide docs using
`docs/contents.md` as the index.
- **Post-commit review:** after functional changes or bug fixes that meet
quality
gates, review changed code and adjacent areas using these heuristics.
quality gates, review changed code and adjacent areas using these heuristics.
- **Separate atomic refactors:** if refactoring is required, implement it in a
separate atomic commit after the functional change and ensure it passes all
relevant gates.
Expand All @@ -161,11 +160,9 @@
integration contracts, persistence, command-line behaviour, network
boundaries, user interface flows, or other system-level behaviour.
- Use property tests with `hypothesis` or `CrossHair` when a change introduces
an
invariant over a range of inputs, states, orderings, or transitions.
an invariant over a range of inputs, states, orderings, or transitions.
- Run relevant unit, behavioural, property, and end-to-end suites before and
after
each change.
after each change.

## Markdown guidance

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ PYLINT_PYPY_SHIM = git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_P
PYLINT = $(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy


.PHONY: help all audit clean build build-release lint lint-python fmt check-fmt \
.PHONY: help all audit clean build build-release crosshair lint lint-python fmt check-fmt \
markdownlint nixie spelling test typecheck $(TOOLS) $(VENV_TOOLS)

.DEFAULT_GOAL := all
Expand Down Expand Up @@ -125,6 +125,9 @@ nixie: ## Validate Mermaid diagrams
test: build $(VENV_TOOLS) ## Run tests
$(UV_ENV) $(ACT_TEST_ENV) $(UV) run pytest -v -n $(PYTEST_XDIST_WORKERS)

crosshair: build ## Model-check pure kernels with CrossHair
$(UV_ENV) RUN_CROSSHAIR=1 $(UV) run pytest -v tests/test_crosshair.py


help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
Expand Down
137 changes: 135 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,136 @@
# df12-python-lints
# 🔎 df12-python-lints

Example package generated from this Copier template.
*Opinionated pylint checkers for code that says what it means.*

A pylint plugin encoding the df12 house style: prefer `match` statements over
imperative type dispatch, make assertions explain themselves, and never silence
a diagnostic without saying why. It also ships `ambrleaks`, a scanner that
catches unredacted values hiding in syrupy snapshots.

______________________________________________________________________

## Why df12-python-lints?

Review feedback is cheapest when a machine gives it before a human has to:

- **Structure over ceremony**: `isinstance` ladders and constant
comparison chains become `match`/`case`, which states the accepted shapes and
values directly.
- **Assertions that testify**: a bare `assert` failure echoes an
expression; an assert with a message names the violated expectation —
invaluable when a property test shrinks to a counterexample.
- **No silent suppressions**: every `noqa`, `pylint: disable`, or
`type: ignore` must record a reason the next reader can audit.
- **Snapshots kept honest**: big inline expected values move into syrupy
snapshots, and the snapshots themselves are swept for hex ids, emails, URLs,
and absolute paths that should have been redacted.

______________________________________________________________________

## Quick start

### Installation

```bash
uv add --dev df12-python-lints
```

### Basic usage

Load the plugin in `pyproject.toml`:

```toml
[tool.pylint.main]
load-plugins = ["df12_python_lints"]
```

Then run pylint as usual:

```bash
pylint my_package tests
```

A dispatch chain like this:

```python
if isinstance(value, dict):
handle_mapping(value)
elif isinstance(value, list):
handle_sequence(value)
```

is reported as:

```text
R9101: Type dispatch on 'value' would be clearer as a match statement
(prefer-structural-pattern-matching)
```

To sweep syrupy snapshots, install the package as a tool and point `ambrleaks`
at your tests:

```bash
uv tool install df12-python-lints
ambrleaks tests
```

______________________________________________________________________

## Features

Twelve pylint messages:

- `prefer-structural-pattern-matching` (R9101) — `isinstance` dispatch on
one subject should be a `match` statement with class patterns.
- `assert-missing-message` (C9102) — every `assert` carries a failure
message naming the violated expectation.
- `prefer-match-over-constant-chain` (R9103) — `if`/`elif` chains
comparing one subject with constants, enum members, or literals should be a
`match` statement over an enumeration.
- `trivial-attribute-wrapper` (R9104) and `trivial-alias-wrapper`
(R9110) — functions with no logic beyond attribute access, a proxied call, or
forwarding to another function add a name without adding behaviour.
- `reexport-by-assignment` (C9105) — re-export with
`from ... import ... as ...` rather than assignment.
- `lint-suppression-without-explanation` (C9106) and
`typecheck-suppression-without-explanation` (C9107) — suppression pragmas
must record a reason.
- `prefer-snapshot-assertion` (R9108) and `prefer-snapshot-substring`
(R9109) — tests asserting against large inline literals or repeatedly probing
substrings should use a syrupy snapshot.
- `prefer-type-statement` (R9111) — module-level type aliases should use
the PEP 695 `type` statement on a 3.12+ baseline.
- `redundant-future-annotations` (C9112) — `from __future__ import annotations`
should be removed on a 3.14+ baseline, where deferred evaluation is the
default.

Both baseline-gated messages respect pylint's `py-version` option.

And one companion tool:

- `ambrleaks` — scans syrupy `.ambr` snapshot files for unredacted hex
strings, UUIDs, emails, phone numbers, URLs, and absolute paths, with entropy
gating, allowlists, and a baseline that survives snapshot regeneration.

______________________________________________________________________

## Learn more

- [Users' Guide](docs/users-guide.md) — every checker, every rule, and
how to suppress findings without touching your snapshots
- [Developers' Guide](docs/developers-guide.md) — contributing and
development workflow
- [Documentation contents](docs/contents.md) — the full documentation set

______________________________________________________________________

## Licence

ISC — see [LICENSE](LICENSE) for details.

______________________________________________________________________

## Contributing

Contributions welcome! Please see [AGENTS.md](AGENTS.md) for guidelines, and run
`make all` before proposing a change.
92 changes: 82 additions & 10 deletions df12_python_lints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,91 @@
"""df12-python-lints package."""
"""df12-python-lints: pylint checkers for df12 Python conventions.

The package is a pylint plugin. Loading it registers nine checkers
providing twelve messages:

- ``prefer-structural-pattern-matching`` (R9101) flags ``isinstance``
dispatch chains better expressed as ``match`` statements;
- ``assert-missing-message`` (C9102) flags ``assert`` statements without a
failure message;
- ``prefer-match-over-constant-chain`` (R9103) flags ``if``/``elif``
chains that only compare one subject with constants, enum members, or
literals;
- ``trivial-attribute-wrapper`` (R9104) flags functions with no logic
beyond attribute access or a proxied call, and
``trivial-alias-wrapper`` (R9110) flags functions that only forward
their arguments to another function;
- ``reexport-by-assignment`` (C9105) flags module-level aliases of
imported names made by assignment rather than import; and
- ``lint-suppression-without-explanation`` (C9106) with
``typecheck-suppression-without-explanation`` (C9107) flag suppression
pragmas that record no reason; and
- ``prefer-snapshot-assertion`` (R9108) with
``prefer-snapshot-substring`` (R9109) flag test assertions better
expressed as syrupy snapshots; and
- ``prefer-type-statement`` (R9111) flags module-level type aliases
better declared with the PEP 695 ``type`` statement, while
``redundant-future-annotations`` (C9112) flags ``from __future__
import annotations`` on a 3.14+ baseline; both respect pylint's
``py-version`` option.

Examples
--------
Load the plugin from a pylint configuration::

[tool.pylint.main]
load-plugins = ["df12_python_lints"]

or from the command line::

pylint --load-plugins=df12_python_lints my_package
"""

from __future__ import annotations

import importlib
import typing as typ

from .assert_messages import AssertMessageChecker
from .constant_chain import ConstantChainChecker
from .future_annotations import FutureAnnotationsChecker
from .match_dispatch import MatchDispatchChecker
from .reexports import ReexportAssignmentChecker
from .snapshot_asserts import SnapshotAssertionChecker
from .suppressions import SuppressionCommentChecker
from .type_aliases import TypeAliasChecker
from .wrappers import TrivialWrapperChecker

if typ.TYPE_CHECKING:
import collections.abc as cabc
from pylint.lint import PyLinter

__all__ = [
"AssertMessageChecker",
"ConstantChainChecker",
"FutureAnnotationsChecker",
"MatchDispatchChecker",
"ReexportAssignmentChecker",
"SnapshotAssertionChecker",
"SuppressionCommentChecker",
"TrivialWrapperChecker",
"TypeAliasChecker",
"register",
]


PACKAGE_NAME = "df12_python_lints"
def register(linter: PyLinter) -> None:
"""Register the df12 checkers with *linter*.

try: # pragma: no cover - Rust optional
rust = importlib.import_module(f"._{PACKAGE_NAME}_rs", package=__name__)
hello = typ.cast("cabc.Callable[[], str]", rust.hello)
except ModuleNotFoundError: # pragma: no cover - Python fallback
from .pure import hello
Pylint calls this entry point when the plugin loads.

__all__ = ["hello"]
Examples
--------
Invoked automatically by ``pylint --load-plugins=df12_python_lints``.
"""
linter.register_checker(MatchDispatchChecker(linter))
linter.register_checker(AssertMessageChecker(linter))
linter.register_checker(ConstantChainChecker(linter))
linter.register_checker(TrivialWrapperChecker(linter))
linter.register_checker(ReexportAssignmentChecker(linter))
linter.register_checker(SuppressionCommentChecker(linter))
linter.register_checker(SnapshotAssertionChecker(linter))
linter.register_checker(TypeAliasChecker(linter))
linter.register_checker(FutureAnnotationsChecker(linter))
Loading
Loading