Skip to main content

mollify_core/
plugins.rs

1//! Framework awareness — the dominant false-positive killer for Python
2//! dead-code analysis (RESEARCH.md §4). A symbol registered with a framework via
3//! a decorator (a Flask/FastAPI route, a Celery task, a pytest fixture, a Django
4//! signal receiver, a click/typer command, a Pydantic validator, …) is *reached*
5//! even with zero in-repo callers.
6//!
7//! Matching is by the decorator's final path segment, so `app.route`,
8//! `router.get`, `bp.cli.command`, and `pytest.fixture` all match. This is a
9//! curated data table — extend it freely; it ships as pure data.
10
11use mollify_parse::Definition;
12
13/// Decorator final-segments that mark a definition as a framework entry point.
14const ENTRY_DECORATORS: &[&str] = &[
15    // Web routes (Flask, FastAPI, Starlette, Sanic, AIOHTTP, Bottle, Quart)
16    "route",
17    "get",
18    "post",
19    "put",
20    "patch",
21    "delete",
22    "head",
23    "options",
24    "websocket",
25    "websocket_route",
26    "middleware",
27    "exception_handler",
28    "on_event",
29    "before_request",
30    "after_request",
31    "errorhandler",
32    // Task queues (Celery, RQ, Dramatiq, Huey, APScheduler)
33    "task",
34    "shared_task",
35    "periodic_task",
36    "actor",
37    "scheduled_job",
38    "on_message",
39    "subscribe",
40    // Tests (pytest) — fixtures are injected by name; hooks register implicitly
41    "fixture",
42    "hookimpl",
43    // Django (signals, admin, template tags/filters, management)
44    "receiver",
45    "register",
46    "display",
47    "action",
48    "simple_tag",
49    "filter",
50    "inclusion_tag",
51    "admin",
52    // CLI (click, typer)
53    "command",
54    "group",
55    "callback",
56    // Pydantic / dataclasses validation hooks
57    "validator",
58    "field_validator",
59    "root_validator",
60    "model_validator",
61    "field_serializer",
62    "model_serializer",
63    "computed_field",
64    // Generic plugin/registry/dispatch patterns
65    "hook",
66    "plugin",
67    "rule",
68    "event",
69    "listener",
70    "handler",
71    "provides",
72    "implementer",
73    "setup",
74    "teardown",
75];
76
77/// True if a single decorator path marks a symbol as a framework entry point.
78pub fn is_framework_entry_decorator(dec: &str) -> bool {
79    let seg = dec.rsplit('.').next().unwrap_or(dec);
80    ENTRY_DECORATORS.contains(&seg)
81}
82
83/// True if any of this definition's decorators marks it as a framework entry.
84pub fn is_framework_entry(def: &Definition) -> bool {
85    def.decorators
86        .iter()
87        .any(|d| is_framework_entry_decorator(d))
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use mollify_parse::DefKind;
94
95    fn def(decorators: &[&str]) -> Definition {
96        Definition {
97            name: "x".into(),
98            kind: DefKind::Function,
99            line: 1,
100            end_line: 2,
101            private_by_convention: false,
102            decorators: decorators.iter().map(|s| s.to_string()).collect(),
103        }
104    }
105
106    #[test]
107    fn recognizes_framework_decorators() {
108        assert!(is_framework_entry(&def(&["app.route"])));
109        assert!(is_framework_entry(&def(&["router.get"])));
110        assert!(is_framework_entry(&def(&["pytest.fixture"])));
111        assert!(is_framework_entry(&def(&["shared_task"])));
112        assert!(is_framework_entry(&def(&["receiver"])));
113        assert!(is_framework_entry(&def(&["cli.command"])));
114        assert!(is_framework_entry(&def(&["field_validator"])));
115    }
116
117    #[test]
118    fn ignores_plain_decorators() {
119        assert!(!is_framework_entry(&def(&["staticmethod"])));
120        assert!(!is_framework_entry(&def(&["functools.lru_cache"])));
121        assert!(!is_framework_entry(&def(&[])));
122    }
123}