Skip to main content

sofie_check/rules/
mod.rs

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