Skip to main content

sofie_check/rules/
flags.rs

1use std::collections::BTreeMap;
2
3use sofie_model::{App, FeatureDeclaration, Module, manifest_file};
4
5use super::{Finding, Rule};
6use crate::Date;
7
8pub(super) fn check_flags(app: &App) -> Vec<Finding> {
9    let mut findings = Vec::new();
10    let mut seen: BTreeMap<&str, &FeatureDeclaration> = BTreeMap::new();
11    for module in &app.modules {
12        for declaration in &module.code.features.declarations {
13            findings.extend(misnamed(module, declaration));
14            findings.extend(outside_manifest(module, declaration));
15            findings.extend(undated(declaration));
16            let Some(name) = declaration.name.as_deref() else {
17                continue;
18            };
19            if let Some(first) = seen.insert(name, declaration) {
20                findings.push(finding(
21                    Rule::FeatureFlag,
22                    declaration,
23                    format!("flag `{name}` is declared twice, here and at {}:{}, so one switch would gate both", first.file.display(), first.line),
24                    "give each flag a name of its own",
25                ));
26            }
27        }
28        findings.extend(unknown_gates(module));
29        findings.extend(unknown_menu_flags(module));
30    }
31    findings
32}
33
34fn misnamed(module: &Module, declaration: &FeatureDeclaration) -> Option<Finding> {
35    let prefix = format!("{}.", module.krate.crate_name);
36    let named = declaration.name.as_deref().filter(|name| {
37        name.strip_prefix(&prefix)
38            .is_some_and(|flag| !flag.is_empty() && !flag.contains('.'))
39    });
40    if named.is_some() {
41        return None;
42    }
43    Some(finding(
44        Rule::FeatureFlag,
45        declaration,
46        format!(
47            "flag `{}` is not named `{prefix}<flag>` by a literal, so its stored state could not be told apart from another module's",
48            declaration.constant.join("::")
49        ),
50        &format!("write `Feature::new(\"{prefix}<flag>\")`"),
51    ))
52}
53
54fn outside_manifest(module: &Module, declaration: &FeatureDeclaration) -> Option<Finding> {
55    let manifest = manifest_file(&module.krate.path);
56    (declaration.file != manifest).then(|| {
57        finding(
58            Rule::FeatureFlag,
59            declaration,
60            format!(
61                "flag `{}` is declared outside the module's manifest {}",
62                declaration.constant.join("::"),
63                manifest.display()
64            ),
65            "move the constant into src/module.rs, beside `MODULE`",
66        )
67    })
68}
69
70fn unknown_gates(module: &Module) -> Vec<Finding> {
71    let features = &module.code.features;
72    features
73        .gates
74        .iter()
75        .filter(|gate| features.declaration_of(gate).is_none())
76        .map(|gate| Finding {
77            rule: Rule::FeatureFlag,
78            file: gate.file.clone(),
79            line: gate.line,
80            message: format!(
81                "`#[feature({})]` on `{}` names no flag this module declares exactly once, so `sofie gen` cannot gate it",
82                gate.feature.join("::"),
83                gate.function.join("::")
84            ),
85            fix: "declare the flag as a `Feature` constant in the module and name it here".to_owned(),
86        })
87        .collect()
88}
89
90fn unknown_menu_flags(module: &Module) -> Vec<Finding> {
91    let menus = module.areas.iter().filter_map(|area| area.menu.as_ref());
92    menus
93        .flat_map(|menu| menu.entries.iter().map(move |entry| (menu, entry)))
94        .filter(|(_, entry)| entry.feature.is_some() && module.menu_feature(entry).is_none())
95        .map(|(menu, entry)| Finding {
96            rule: Rule::FeatureFlag,
97            file: menu.file.clone(),
98            line: entry.line,
99            message: format!(
100                "menu entry `{}` is shown by `{}`, which names no flag this module declares exactly once",
101                entry.label,
102                entry.feature.as_deref().unwrap_or_default()
103            ),
104            fix: "declare the flag as a `Feature` constant in the module's manifest and name it here".to_owned(),
105        })
106        .collect()
107}
108
109#[must_use]
110pub fn check_flag_ages(app: &App, today: Date) -> Vec<Finding> {
111    let declarations = app
112        .modules
113        .iter()
114        .flat_map(|module| &module.code.features.declarations);
115    declarations
116        .filter_map(|declaration| {
117            let added = Date::parse(declaration.added.as_deref()?)?;
118            let age = today.days_since(added);
119            (age > i64::from(declaration.retire_after_days)).then(|| finding(
120                Rule::FlagAge,
121                declaration,
122                format!(
123                    "flag `{}` was added {age} days ago, past the {} days it declares",
124                    declaration.constant.join("::"),
125                    declaration.retire_after_days
126                ),
127                "switch it fully on or off, then remove the flag and keep only the code that state runs; the build shows every use left",
128            ))
129        })
130        .collect()
131}
132
133fn undated(declaration: &FeatureDeclaration) -> Option<Finding> {
134    let constant = declaration.constant.join("::");
135    let message = match declaration.added.as_deref() {
136        Some(added) if Date::parse(added).is_some() => return None,
137        Some(added) => {
138            format!("flag `{constant}` was added on `{added}`, which is no `YYYY-MM-DD` day")
139        }
140        None => format!(
141            "flag `{constant}` declares no literal day it was added, so `sofie check` cannot tell when it outlives its {} days",
142            declaration.retire_after_days
143        ),
144    };
145    Some(finding(
146        Rule::FlagAge,
147        declaration,
148        message,
149        "write `.added(\"YYYY-MM-DD\")` with the day the flag was added",
150    ))
151}
152
153fn finding(rule: Rule, declaration: &FeatureDeclaration, message: String, fix: &str) -> Finding {
154    Finding {
155        rule,
156        file: declaration.file.clone(),
157        line: declaration.line,
158        message,
159        fix: fix.to_owned(),
160    }
161}