sofie_check/rules/
flags.rs1use 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}