sofie_cli/
check_command.rs1use std::path::Path;
2
3use sofie_check::{
4 AllowList, Date, Finding, MAX_UNUSED_PUBLIC_RATIO, Rule, Screens, check_dead_code,
5};
6use sofie_model::{App, Crate};
7
8use crate::clients::has_clients;
9use crate::gen_command::{GenError, GenMode, GenOutcome, sync_generated};
10use crate::translations::AppLanguages;
11
12const DERIVES_PACKAGE: &str = "sofie-macros";
13
14pub fn check_app(root: &Path) -> Result<Vec<Finding>, GenError> {
16 let app = sofie_model::load(root).map_err(GenError::Model)?;
17 check_model(root, &app)
18}
19
20pub(crate) fn check_model(root: &Path, app: &App) -> Result<Vec<Finding>, GenError> {
21 let mut findings = check_loaded_app(root, app, &Screens::read(&app.modules))?;
22 findings.extend(sofie_check::check_flag_ages(app, Date::today()));
24 findings.extend(client_feature(root, app)?);
25 sofie_check::sort_findings(&mut findings);
26 Ok(findings)
27}
28
29fn client_feature(root: &Path, app: &App) -> Result<Vec<Finding>, GenError> {
32 if !has_clients(app) {
33 return Ok(Vec::new());
34 }
35 let features = sofie_model::framework_features(root).map_err(GenError::Model)?;
36 Ok(sofie_check::check_client_feature(app, &features))
37}
38
39pub(crate) fn check_loaded_app(
40 root: &Path,
41 app: &App,
42 screens: &Screens,
43) -> Result<Vec<Finding>, GenError> {
44 let languages = AppLanguages::read(app)?;
45 let mut findings = sofie_check::check(app, screens);
46 if let Some(languages) = &languages {
47 findings.extend(languages.findings(app, screens)?);
48 }
49 if app.structure_ceilings {
50 findings.extend(sofie_check::check_structure(root).map_err(GenError::Model)?);
51 }
52 if !findings.is_empty() {
53 sofie_check::sort_findings(&mut findings);
54 return Ok(findings);
55 }
56 let stale = [
57 stale_files(
58 sync_generated(app, screens, languages.as_ref(), GenMode::Check)?,
59 Rule::StaleWiring,
60 &format!("run `sofie gen {}`", root.display()),
61 ),
62 stale_docs(root, app)?,
63 ]
64 .concat();
65 if !stale.is_empty() {
66 return Ok(stale);
67 }
68 dead_code(root, app)
69}
70
71#[cfg(feature = "docs")]
72fn stale_docs(root: &Path, app: &App) -> Result<Vec<Finding>, GenError> {
73 if !crate::docs::keeps_docs(app) {
74 return Ok(Vec::new());
75 }
76 Ok(stale_files(
77 crate::docs::sync_docs(app, GenMode::Check)?,
78 Rule::StaleDocs,
79 &format!("run `sofie docs {}`", root.display()),
80 ))
81}
82
83#[cfg(not(feature = "docs"))]
84#[expect(
85 clippy::unnecessary_wraps,
86 reason = "the same signature as with `docs`"
87)]
88fn stale_docs(_root: &Path, _app: &App) -> Result<Vec<Finding>, GenError> {
89 Ok(Vec::new())
90}
91
92fn stale_files(outcomes: Vec<GenOutcome>, rule: Rule, fix: &str) -> Vec<Finding> {
93 outcomes
94 .into_iter()
95 .filter_map(|outcome| match outcome {
96 GenOutcome::Stale(file) => Some(Finding {
97 rule,
98 file,
99 line: 1,
100 message: "the generated file does not match the app".to_owned(),
101 fix: fix.to_owned(),
102 }),
103 GenOutcome::Written(_) | GenOutcome::Removed(_) | GenOutcome::UpToDate(_) => None,
104 })
105 .collect()
106}
107
108fn dead_code(root: &Path, app: &App) -> Result<Vec<Finding>, GenError> {
109 let derives = sofie_model::dependency_crate(root, DERIVES_PACKAGE).map_err(GenError::Model)?;
110 let allowed = AllowList::read(root).map_err(GenError::Model)?;
111 let checked: Vec<&Crate> = std::iter::once(&app.app)
112 .chain(
113 app.modules
114 .iter()
115 .flat_map(|module| std::iter::once(&module.krate).chain(&module.contract)),
116 )
117 .collect();
118 let context: Vec<&Crate> = derives.iter().collect();
119 let mut findings = check_dead_code(&checked, &context, &allowed, MAX_UNUSED_PUBLIC_RATIO)
120 .map_err(GenError::Model)?;
121 sofie_check::sort_findings(&mut findings);
122 Ok(findings)
123}