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 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}