Skip to main content

tramli_plugins/lint/
mod.rs

1use tramli::{FlowDefinition, FlowState, TransitionType};
2use crate::api::{FindingLocation, PluginReport};
3
4/// Flow policy — a lint rule applied to a flow definition.
5pub type FlowPolicy<S> = Box<dyn Fn(&FlowDefinition<S>, &mut PluginReport) + Send + Sync>;
6
7/// Default lint policies.
8pub fn default_policies<S: FlowState>() -> Vec<FlowPolicy<S>> {
9    vec![
10        Box::new(warn_terminal_with_outgoing),
11        Box::new(warn_too_many_externals),
12        Box::new(warn_dead_produced_data),
13        Box::new(warn_overwide_processors),
14    ]
15}
16
17fn warn_terminal_with_outgoing<S: FlowState>(def: &FlowDefinition<S>, report: &mut PluginReport) {
18    for state in def.terminal_states() {
19        if !def.transitions_from(*state).is_empty() {
20            report.warn_at(
21                "policy/terminal-outgoing",
22                &format!("terminal state {:?} has outgoing transitions", state),
23                FindingLocation::State { state: format!("{:?}", state) },
24            );
25        }
26    }
27}
28
29fn warn_too_many_externals<S: FlowState>(def: &FlowDefinition<S>, report: &mut PluginReport) {
30    for state in S::all_states() {
31        let externals: Vec<_> = def.transitions_from(*state)
32            .into_iter()
33            .filter(|t| t.transition_type == TransitionType::External)
34            .collect();
35        if externals.len() > 3 {
36            report.warn_at(
37                "policy/external-count",
38                &format!("state {:?} has {} external transitions", state, externals.len()),
39                FindingLocation::State { state: format!("{:?}", state) },
40            );
41        }
42    }
43}
44
45fn warn_dead_produced_data<S: FlowState>(def: &FlowDefinition<S>, report: &mut PluginReport) {
46    let dead = def.data_flow_graph().dead_data();
47    for type_id in dead {
48        let name = def.data_flow_graph().type_name(&type_id);
49        report.warn_at(
50            "policy/dead-data",
51            &format!("produced but never consumed: {}", name),
52            FindingLocation::Data { data_key: name.to_string() },
53        );
54    }
55}
56
57fn warn_overwide_processors<S: FlowState>(def: &FlowDefinition<S>, report: &mut PluginReport) {
58    for t in &def.transitions {
59        if let Some(ref p) = t.processor {
60            if p.produces().len() > 3 {
61                report.warn_at(
62                    "policy/overwide-processor",
63                    &format!("{} produces {} types; consider splitting it", p.name(), p.produces().len()),
64                    FindingLocation::Transition {
65                        from_state: format!("{:?}", t.from),
66                        to_state: format!("{:?}", t.to),
67                    },
68                );
69            }
70        }
71    }
72}
73
74/// Policy lint plugin — applies lint policies to a flow definition.
75pub struct PolicyLintPlugin<S: FlowState> {
76    policies: Vec<FlowPolicy<S>>,
77}
78
79impl<S: FlowState> PolicyLintPlugin<S> {
80    pub fn new(policies: Vec<FlowPolicy<S>>) -> Self {
81        Self { policies }
82    }
83
84    pub fn defaults() -> Self {
85        Self::new(default_policies())
86    }
87
88    pub fn analyze(&self, definition: &FlowDefinition<S>, report: &mut PluginReport) {
89        for policy in &self.policies {
90            policy(definition, report);
91        }
92    }
93}