Skip to main content

sofie_cli/
check_command.rs

1use 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
14/// Generated files are rendered only from a model the rules accept, and dead code is judged only against current ones.
15pub 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    // Not in the build script: a build must not start failing as days pass.
23    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
29/// Resolving the features reads the whole dependency graph, so only an app with a client area
30/// pays for it.
31fn 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}