Skip to main content

sofie_check/rules/
mod.rs

1mod actions;
2mod allowances;
3mod bindings;
4mod clients;
5mod clock;
6mod constraints;
7mod cross_module;
8mod database;
9mod dead_code;
10mod entitlements;
11mod environment;
12mod exposed;
13mod flags;
14mod fragments;
15mod islands;
16mod locking;
17mod mails;
18mod manifest;
19mod members;
20mod messages;
21mod migration;
22mod module_manifest;
23mod names;
24mod outgoing;
25mod own_contract;
26mod raw_sql;
27mod registrations;
28mod remote;
29mod routes;
30mod safe_methods;
31mod sources;
32mod tasks;
33mod tiers;
34mod visibility;
35mod wiring;
36
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40use sofie_model::App;
41
42use crate::screens::Screens;
43
44pub use allowances::{ALLOW_FILE, AllowList};
45pub use clients::check_client_feature;
46pub use dead_code::{MAX_UNUSED_PUBLIC_RATIO, check_dead_code};
47pub use flags::check_flag_ages;
48pub use migration::Schema as MigratedSchema;
49pub use names::missing_gen_exports;
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub enum Rule {
53    ActionInput,
54    AppDependency,
55    AreaDefinition,
56    AreaPrefix,
57    BehaviorColumn,
58    ClientFeature,
59    CommentDensity,
60    Config,
61    CrossModuleSql,
62    DatabaseDriver,
63    EmbeddedPath,
64    Entitlement,
65    Environment,
66    EventInContract,
67    ExposedField,
68    FeatureFlag,
69    FlagAge,
70    Island,
71    IslandProps,
72    GodType,
73    JobName,
74    ListenerEvent,
75    MailTemplate,
76    MemberHome,
77    MigrationLock,
78    MirrorField,
79    MissingScreen,
80    ModuleDependency,
81    ModuleManifest,
82    Outgoing,
83    OwnContract,
84    PlatformListener,
85    QueuedRoute,
86    RawSql,
87    RemoteCall,
88    RemoteDependency,
89    RouteConflict,
90    RouteKind,
91    RouteName,
92    ScheduleEach,
93    ScheduleInterval,
94    ScheduleZone,
95    SchemaConstraint,
96    ScreenBinding,
97    ScreenOverride,
98    ScreenTemplate,
99    StaleAllowance,
100    StaleDocs,
101    StaleWiring,
102    SystemTime,
103    TaskRunner,
104    ThenScreen,
105    ThenScreenFragment,
106    TierDirection,
107    UndeclaredGen,
108    UndeclaredRoutes,
109    UnknownArea,
110    UnusedDependency,
111    UnusedItem,
112    UnusedRatio,
113    Visibility,
114    WritingGet,
115}
116
117impl Rule {
118    #[must_use]
119    pub fn name(self) -> &'static str {
120        match self {
121            Self::ActionInput => "action-input",
122            Self::AppDependency => "app-dependency",
123            Self::AreaDefinition => "area-definition",
124            Self::AreaPrefix => "area-prefix",
125            Self::BehaviorColumn => "behavior-column",
126            Self::ClientFeature => "client-feature",
127            Self::CommentDensity => "comment-density",
128            Self::Config => "config",
129            Self::CrossModuleSql => "cross-module-sql",
130            Self::DatabaseDriver => "database-driver",
131            Self::EmbeddedPath => "embedded-path",
132            Self::Entitlement => "entitlement",
133            Self::Environment => "environment",
134            Self::EventInContract => "event-in-contract",
135            Self::ExposedField => "exposed-field",
136            Self::FeatureFlag => "feature-flag",
137            Self::FlagAge => "flag-age",
138            Self::GodType => "god-type",
139            Self::JobName => "job-name",
140            Self::Island => "island",
141            Self::IslandProps => "island-props",
142            Self::ListenerEvent => "listener-event",
143            Self::MailTemplate => "mail-template",
144            Self::MemberHome => "member-home",
145            Self::MigrationLock => "migration-lock",
146            Self::MirrorField => "mirror-field",
147            Self::MissingScreen => "missing-screen",
148            Self::ModuleDependency => "module-dependency",
149            Self::ModuleManifest => "module-manifest",
150            Self::Outgoing => "outgoing",
151            Self::OwnContract => "own-contract",
152            Self::PlatformListener => "platform-listener",
153            Self::QueuedRoute => "queued-route",
154            Self::RawSql => "raw-sql-in-queries-only",
155            Self::RemoteCall => "remote-call",
156            Self::RemoteDependency => "remote-dependency",
157            Self::RouteConflict => "route-conflict",
158            Self::RouteKind => "route-kind",
159            Self::RouteName => "route-name",
160            Self::ScreenBinding => "screen-binding",
161            Self::ScheduleEach => "schedule-each",
162            Self::ScheduleInterval => "schedule-interval",
163            Self::ScheduleZone => "schedule-zone",
164            Self::SchemaConstraint => "schema-constraint",
165            Self::ScreenOverride => "screen-override",
166            Self::ScreenTemplate => "screen-template",
167            Self::StaleAllowance => "stale-allowance",
168            Self::StaleDocs => "stale-docs",
169            Self::StaleWiring => "stale-wiring",
170            Self::SystemTime => "system-time",
171            Self::TaskRunner => "task-runner",
172            Self::ThenScreen => "then-screen",
173            Self::ThenScreenFragment => "then-screen-fragment",
174            Self::TierDirection => "tier-direction",
175            Self::UndeclaredGen => "undeclared-gen",
176            Self::UndeclaredRoutes => "undeclared-routes",
177            Self::UnknownArea => "unknown-area",
178            Self::UnusedDependency => "unused-dependency",
179            Self::UnusedItem => "unused-item",
180            Self::UnusedRatio => "unused-ratio",
181            Self::Visibility => "visibility",
182            Self::WritingGet => "writing-get",
183        }
184    }
185}
186
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct Finding {
189    pub rule: Rule,
190    pub file: PathBuf,
191    pub line: usize,
192    pub message: String,
193    pub fix: String,
194}
195
196impl Finding {
197    fn render(&self, root: &Path) -> String {
198        let file = self.file.strip_prefix(root).unwrap_or(&self.file);
199        format!(
200            "error[{}]: {}\n  --> {}:{}\n   = help: {}\n",
201            self.rule.name(),
202            self.message,
203            file.display(),
204            self.line,
205            self.fix
206        )
207    }
208}
209
210impl fmt::Display for Finding {
211    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212        formatter.write_str(&self.render(Path::new("")))
213    }
214}
215
216#[must_use]
217pub fn check(app: &App, screens: &Screens) -> Vec<Finding> {
218    let migrations = migration::Migrations::read(&app.modules);
219    let mut findings = manifest::check_manifests(app);
220    findings.extend(module_manifest::check_module_manifests(&app.modules));
221    findings.extend(sources::check_sources(app));
222    findings.extend(outgoing::check_gateways(&app.modules));
223    findings.extend(routes::check_routes(app));
224    findings.extend(safe_methods::check_safe_methods(&app.modules));
225    findings.extend(own_contract::check_own_contract(&app.modules));
226    findings.extend(fragments::check_fragments(&app.modules, screens));
227    findings.extend(mails::check_mails(&app.modules));
228    findings.extend(bindings::check_bindings(&app.modules, screens));
229    findings.extend(actions::check_actions(app, screens));
230    findings.extend(islands::check_islands(&app.modules, screens));
231    findings.extend(exposed::check_exposed(&app.modules));
232    findings.extend(exposed::check_mirrors(&app.modules));
233    findings.extend(names::check_names(app));
234    findings.extend(registrations::check_registrations(app));
235    findings.extend(raw_sql::check_raw_sql(&app.modules));
236    findings.extend(constraints::check_constraints(&app.modules, &migrations));
237    findings.extend(cross_module::check_cross_module(&app.modules, &migrations));
238    findings.extend(locking::check_locking(&app.modules, &migrations));
239    findings.extend(messages::check_messages(app));
240    findings.extend(messages::check_queued_routes(app));
241    findings.extend(members::check_member_areas(app));
242    findings.extend(visibility::check_visibility(&app.modules));
243    findings.extend(wiring::check_wiring(app));
244    findings.extend(flags::check_flags(app));
245    findings.extend(entitlements::check_entitlements(app, screens));
246    findings.extend(remote::check_remote_modules(app));
247    findings.extend(tasks::check_task_runner(app));
248    sort_findings(&mut findings);
249    // A handler routed twice has its rendered screens checked twice.
250    findings.dedup();
251    findings
252}
253
254pub fn sort_findings(findings: &mut [Finding]) {
255    findings.sort_by(|left, right| {
256        (&left.file, left.line, left.rule.name()).cmp(&(&right.file, right.line, right.rule.name()))
257    });
258}
259
260#[must_use]
261pub fn report(root: &Path, findings: &[Finding]) -> String {
262    if findings.is_empty() {
263        return "ok\n".to_owned();
264    }
265    let mut blocks: Vec<String> = findings
266        .iter()
267        .map(|finding| finding.render(root) + "\n")
268        .collect();
269    let noun = if findings.len() == 1 {
270        "problem"
271    } else {
272        "problems"
273    };
274    blocks.push(format!("{} {noun}\n", findings.len()));
275    blocks.concat()
276}
277
278#[cfg(test)]
279mod tests {
280    use std::path::{Path, PathBuf};
281
282    use super::{Finding, Rule, report};
283
284    fn finding(rule: Rule, file: &str, line: usize) -> Finding {
285        Finding {
286            rule,
287            file: PathBuf::from(file),
288            line,
289            message: format!("{} is broken", rule.name()),
290            fix: format!("fix {}", rule.name()),
291        }
292    }
293
294    #[test]
295    fn reports_findings_relative_to_the_root_with_a_count() {
296        let findings = [
297            finding(
298                Rule::RouteKind,
299                "shop/modules/order/src/areas/web/routes.rs",
300                5,
301            ),
302            finding(Rule::StaleWiring, "shop/app/src/wiring.rs", 1),
303        ];
304
305        let text = report(Path::new("shop"), &findings);
306
307        assert_eq!(
308            text,
309            "error[route-kind]: route-kind is broken\n  --> modules/order/src/areas/web/routes.rs:5\n   = help: fix route-kind\n\n\
310             error[stale-wiring]: stale-wiring is broken\n  --> app/src/wiring.rs:1\n   = help: fix stale-wiring\n\n\
311             2 problems\n"
312        );
313    }
314
315    #[test]
316    fn reports_ok_when_clean() {
317        assert_eq!(report(Path::new("."), &[]), "ok\n");
318    }
319}