Skip to main content

mollify_core/
explain.rs

1//! `mollify explain <rule>` — human-readable semantics for a rule id, with no
2//! analysis run. Keeps the "evidence, not decisions" contract legible: every
3//! rule states what it proves, its confidence ceiling, and how to act on it.
4
5/// Return the explanation for a rule id, or `None` if unknown.
6pub fn text(rule: &str) -> Option<&'static str> {
7    let t = match rule {
8        "unused-file" => {
9            "A module that nothing reachable from an entry point imports. \
10            Confidence: certain when there is no dynamic import sink in the project. \
11            Action: delete the file, or mark its module as an entry point."
12        }
13        "unused-import" => {
14            "An imported name that is never referenced outside its own import in \
15            the module. Confidence: certain in a regular module with no dynamic \
16            sink (auto-fixable); uncertain in `__init__.py` (likely a re-export). \
17            Action: remove the import."
18        }
19        "unused-variable" => {
20            "A local variable assigned but never read in its function (ruff F841). \
21            Confidence: likely. Not auto-fixed (the right-hand side may have side \
22            effects). Action: remove it, or prefix with `_`."
23        }
24        "unused-parameter" => {
25            "A function parameter never used in the body. Confidence: uncertain \
26            (it may satisfy an interface/override/callback signature). Action: \
27            remove it or prefix with `_`."
28        }
29        "unused-export" => {
30            "A top-level function/class never referenced outside its own \
31            module and not listed in `__all__`. Confidence: likely (dynamic access via \
32            getattr downgrades it). Action: remove it or make it private."
33        }
34        "unused-method" => {
35            "A class method never referenced anywhere as an attribute \
36            (`obj.m`/`self.m`/`Class.m`). Confidence: likely for private (`_m`), \
37            uncertain for public (may be an override/duck-typed/external API). \
38            Skips dunders, properties, static/class/abstract methods, and \
39            framework-registered methods. Action: remove it, or confirm the API use."
40        }
41        "unused-attribute" => {
42            "A class-level attribute/constant never referenced as an attribute \
43            and never read as a bare name. Confidence: likely for private, uncertain \
44            for public. Skips dataclass/Pydantic/NamedTuple/TypedDict fields. \
45            Action: remove it, or confirm dynamic use."
46        }
47        "unused-enum-member" => {
48            "An `enum.Enum` member never referenced. Confidence: uncertain — enums \
49            are often accessed dynamically (`Color[name]`, `Color(value)`, iteration, \
50            serialization). Action: remove it, or confirm dynamic/serialized use."
51        }
52        "unreachable-code" => {
53            "A statement that can never execute because it follows an \
54            unconditional terminator (`return`/`raise`/`break`/`continue`/`sys.exit()`) \
55            in the same block. Confidence: certain — provable syntactically. \
56            Action: remove the dead statement."
57        }
58        "private-type-leak" => {
59            "A public function/method whose signature references a private \
60            (`_Name`) type a caller cannot name (intentional `TypeVar`s are \
61            excluded). Confidence: likely. Action: make the type public, or stop \
62            exposing it in the public signature."
63        }
64        "unused-dependency" => {
65            "A distribution declared in pyproject/requirements but never \
66            imported. Confidence: likely. Action: remove it from your dependency list."
67        }
68        "transitive-dependency" => {
69            "A package imported and installed, but only because another dependency \
70            pulls it in (not declared directly). Confidence: likely. Action: add \
71            it to your direct dependencies so it survives the transitive dep changing."
72        }
73        "missing-dependency" => {
74            "A third-party module imported but absent from your declared \
75            dependencies (not stdlib, not first-party). Action: add it to your project metadata."
76        }
77        "misplaced-dev-dependency" => {
78            "A distribution declared only in a dev/test group (PEP 735 \
79            `dependency-groups`, Poetry/uv/pdm dev deps) but imported from \
80            production (non-test) code (deptry DEP004). Confidence: likely. \
81            Action: move it to your runtime dependencies."
82        }
83        "unresolved-import" => {
84            "An import that looks internal — relative (`from . import x`) or under \
85            a first-party top-level package — but resolves to no module in the \
86            project. Confidence: certain for relative imports, likely for absolute \
87            (path hacks exist). Action: fix the module path or remove the broken import."
88        }
89        "duplicate-export" => {
90            "An `__init__.py` re-exports the same name from two different \
91            modules; the later import silently shadows the earlier, so one \
92            re-export is dead and the public API is ambiguous. Confidence: likely. \
93            Action: keep a single source for the name."
94        }
95        "private-import" => {
96            "A module imports another *package*'s private (`_name`) symbol, \
97            reaching past its public API (tach/knip interface enforcement). \
98            Intra-package and relative imports are not flagged. Confidence: likely. \
99            Action: import via the package's public API, or make the name public."
100        }
101        "circular-dependency" => {
102            "A cycle of modules that import one another (Tarjan SCC). \
103            Confidence: certain — provable from static imports. Action: extract shared code \
104            to a lower module, or defer one import into function scope."
105        }
106        "layer-violation" => {
107            "A module imports a higher architectural layer than its own \
108            (per `architecture.layers`). Confidence: certain. Action: invert or relocate \
109            the dependency so lower layers never depend on higher ones."
110        }
111        "forbidden-import" => {
112            "An import that violates a declarative `contracts.forbidden` rule in \
113            `.mollifyrc` (module must not depend on another). Confidence: certain. \
114            Action: invert or relocate the dependency."
115        }
116        "independence-violation" => {
117            "Two modules declared independent (`contracts.independent`) import each \
118            other. Confidence: certain. Action: extract shared code to a common \
119            lower module."
120        }
121        "high-complexity" => {
122            "A function whose cyclomatic or cognitive complexity exceeds the \
123            configured threshold. Action: decompose it; extract helpers and flatten branches."
124        }
125        "duplication" => {
126            "A token sequence repeated across locations (exact clone found via a \
127            suffix array + LCP). Action: extract the shared logic into one definition."
128        }
129        "cold-code" => {
130            "A statically reachable function with zero executed lines in the \
131            supplied coverage report. Confidence: likely. Action: verify it is dead, then remove."
132        }
133        "commented-code" => {
134            "A comment whose text parses as Python code (dead code left in a \
135            comment). Confidence: likely. Action: delete it — version control \
136            remembers it."
137        }
138        "low-cohesion" => {
139            "A class whose methods share few instance attributes (high LCOM*) — \
140            it likely does several unrelated jobs. Confidence: uncertain. Action: \
141            split it into cohesive smaller classes."
142        }
143        "hotspot" => {
144            "A file that is both high-churn (git history) and high-complexity — the \
145            riskiest code to change. Action: prioritize it for refactoring and test coverage."
146        }
147        "untyped-function" | "untyped-public" => {
148            "A public function with no parameter or \
149            return type annotations. Action: add type hints to harden the public surface."
150        }
151        "respect-policy" | "policy-violation" => {
152            "A declarative `.mollifyrc` policy was \
153            violated (a forbidden import or call appeared). Confidence: certain. Action: remove \
154            or relocate the forbidden construct."
155        }
156        "dangerous-eval" => {
157            "A call to `eval`/`exec` on a non-literal argument. Action: replace \
158            with an explicit, safe parser or dispatch table."
159        }
160        "subprocess-shell-true" => {
161            "A subprocess call with `shell=True`. Action: pass an argv \
162            list instead of a shell string to avoid injection."
163        }
164        "unsafe-yaml-load" => "`yaml.load` without a safe loader. Action: use `yaml.safe_load`.",
165        "unsafe-deserialization" => {
166            "Deserializing untrusted data with pickle/marshal/shelve. \
167            Action: use a safe format such as JSON."
168        }
169        "tls-verify-disabled" => {
170            "TLS verification disabled (`verify=False`). Action: keep \
171            verification on; pin a CA bundle if needed."
172        }
173        "vulnerable-dependency" => {
174            "A pinned/locked dependency version falls in a known-vulnerable range \
175            from the local advisory DB (`.mollify/advisories.json`). Confidence: \
176            certain given the DB. Action: upgrade out of the affected range; refresh \
177            the DB with scripts/fetch-advisories.py."
178        }
179        "hardcoded-secret" => {
180            "A literal that looks like a credential assigned to a \
181            secret-named variable. Action: load it from the environment or a secret manager."
182        }
183        "weak-hash" => {
184            "Use of a broken hash (md5/sha1) (CWE-327). Action: use sha256+ \
185            or pass usedforsecurity=False if it's a non-security checksum."
186        }
187        "weak-cipher" => {
188            "A broken/weak cipher or ECB mode (CWE-327). Action: use an \
189            authenticated cipher such as AES-GCM or ChaCha20-Poly1305."
190        }
191        "insecure-random" => {
192            "`random` is not cryptographically secure (CWE-330). Action: use \
193            the `secrets` module for tokens/keys/nonces."
194        }
195        "sql-injection" => {
196            "SQL built from an f-string/concatenation/.format passed to an \
197            execute-style sink (CWE-89). Action: use parameterized queries."
198        }
199        "request-without-timeout" => {
200            "An HTTP request without a timeout can block indefinitely \
201            (CWE-400). Action: pass timeout=."
202        }
203        "flask-debug-true" => {
204            "A web app run with debug=True ships the interactive debugger — \
205            remote code execution in production (CWE-94). Action: drive debug \
206            from config/env and never enable it in production."
207        }
208        "jinja2-autoescape-false" => {
209            "A Jinja2 Environment created with autoescape=False risks XSS \
210            (CWE-79). Action: enable autoescaping (or use select_autoescape)."
211        }
212        "try-except-pass" => {
213            "A broad `except: pass` (bare or Exception/BaseException) silently \
214            swallows all errors (CWE-703). Confidence: uncertain. Action: log or \
215            handle the error, or narrow the exception type."
216        }
217        _ => return None,
218    };
219    Some(t)
220}
221
222/// Every rule id mollify can emit, for `mollify explain` with no argument.
223pub const RULES: &[&str] = &[
224    "unused-file",
225    "unused-export",
226    "unused-import",
227    "unused-variable",
228    "unused-parameter",
229    "unused-method",
230    "unused-attribute",
231    "unused-enum-member",
232    "unreachable-code",
233    "unused-dependency",
234    "missing-dependency",
235    "transitive-dependency",
236    "misplaced-dev-dependency",
237    "unresolved-import",
238    "duplicate-export",
239    "private-import",
240    "circular-dependency",
241    "layer-violation",
242    "forbidden-import",
243    "independence-violation",
244    "high-complexity",
245    "duplication",
246    "cold-code",
247    "commented-code",
248    "hotspot",
249    "low-cohesion",
250    "untyped-function",
251    "private-type-leak",
252    "policy-violation",
253    "dangerous-eval",
254    "subprocess-shell-true",
255    "unsafe-yaml-load",
256    "unsafe-deserialization",
257    "tls-verify-disabled",
258    "hardcoded-secret",
259    "weak-hash",
260    "weak-cipher",
261    "insecure-random",
262    "sql-injection",
263    "request-without-timeout",
264    "flask-debug-true",
265    "jinja2-autoescape-false",
266    "try-except-pass",
267    "vulnerable-dependency",
268];
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn known_rules_explain_and_unknown_is_none() {
276        assert!(text("circular-dependency").unwrap().contains("cycle"));
277        assert!(text("layer-violation").is_some());
278        assert!(text("not-a-rule").is_none());
279        // Every advertised rule has prose.
280        for r in RULES {
281            assert!(text(r).is_some(), "no explanation for {r}");
282        }
283    }
284}