1pub 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
222pub 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 for r in RULES {
281 assert!(text(r).is_some(), "no explanation for {r}");
282 }
283 }
284}