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