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