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