fallow_engine/baseline/
install_state.rs1use std::path::Path;
15
16use fallow_types::semantic::SemanticAnalysisIdentity;
17use fallow_types::workspace::{NodeModulesState, WorkspaceDiagnostic, WorkspaceDiagnosticKind};
18
19use super::BaselineData;
20
21#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
27pub(super) struct BaselineAnalysisIdentity {
28 #[serde(flatten)]
29 pub(super) semantic: SemanticAnalysisIdentity,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub(super) node_modules: Option<NodeModulesState>,
34}
35
36impl From<SemanticAnalysisIdentity> for BaselineAnalysisIdentity {
37 fn from(semantic: SemanticAnalysisIdentity) -> Self {
38 Self {
39 semantic,
40 node_modules: None,
41 }
42 }
43}
44
45impl BaselineData {
46 #[must_use]
48 pub const fn with_node_modules(mut self, state: NodeModulesState) -> Self {
49 self.analysis_identity.node_modules = Some(state);
50 self
51 }
52}
53
54#[must_use]
60pub fn saved_node_modules_state(json: &str) -> Option<NodeModulesState> {
61 let value = serde_json::from_str::<serde_json::Value>(json).ok()?;
62 let state = value.get("analysis_identity")?.get("node_modules")?;
63 serde_json::from_value(state.clone()).ok()
64}
65
66#[must_use]
72pub fn install_state_diagnostic(
73 root: &Path,
74 baseline_path: &Path,
75 json: &str,
76 run: NodeModulesState,
77) -> Option<WorkspaceDiagnostic> {
78 let saved = saved_node_modules_state(json)?;
79 (saved != run).then(|| {
80 WorkspaceDiagnostic::new(
81 root,
82 dunce::canonicalize(baseline_path).unwrap_or_else(|_| baseline_path.to_path_buf()),
83 WorkspaceDiagnosticKind::BaselineInstallStateMismatch {
84 baseline_node_modules: saved,
85 run_node_modules: run,
86 },
87 )
88 })
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::results::AnalysisResults;
95
96 fn saved(state: Option<NodeModulesState>) -> String {
97 let data = BaselineData::from_results(&AnalysisResults::default(), Path::new("/p"));
98 let data = match state {
99 Some(state) => data.with_node_modules(state),
100 None => data,
101 };
102 serde_json::to_string(&data).expect("serializes")
103 }
104
105 #[test]
106 fn a_saved_baseline_records_the_install_state_inside_the_analysis_identity() {
107 let json = saved(Some(NodeModulesState::Present));
108 let value: serde_json::Value = serde_json::from_str(&json).expect("json");
109 assert_eq!(value["analysis_identity"]["node_modules"], "present");
110 assert_eq!(value["analysis_identity"]["mode"], "syntactic");
111 assert_eq!(
112 saved_node_modules_state(&json),
113 Some(NodeModulesState::Present)
114 );
115 let loaded: BaselineData = serde_json::from_str(&json).expect("loads");
116 assert_eq!(
117 loaded.analysis_identity(),
118 &SemanticAnalysisIdentity::syntactic()
119 );
120 }
121
122 #[test]
123 fn a_legacy_baseline_has_no_install_state_and_still_loads() {
124 let json = saved(None);
125 assert!(!json.contains("node_modules"));
126 assert_eq!(saved_node_modules_state(&json), None);
127 assert!(serde_json::from_str::<BaselineData>(&json).is_ok());
128 let without_identity = r#"{"unused_files":[],"unused_exports":[],"unused_types":[],
129 "unused_dependencies":[],"unused_dev_dependencies":[]}"#;
130 assert!(serde_json::from_str::<BaselineData>(without_identity).is_ok());
131 assert_eq!(saved_node_modules_state(without_identity), None);
132 assert_eq!(saved_node_modules_state("not json"), None);
133 }
134
135 #[test]
138 fn the_semantic_identity_ignores_the_install_state_key() {
139 let json = saved(Some(NodeModulesState::Missing));
140 let value: serde_json::Value = serde_json::from_str(&json).expect("json");
141 let identity: SemanticAnalysisIdentity =
142 serde_json::from_value(value["analysis_identity"].clone()).expect("older read");
143 assert_eq!(identity, SemanticAnalysisIdentity::syntactic());
144 }
145
146 #[test]
147 fn only_a_known_and_different_state_gives_a_diagnostic() {
148 let root = Path::new("/p");
149 let path = root.join("fallow-baseline.json");
150 let present = saved(Some(NodeModulesState::Present));
151 assert!(
152 install_state_diagnostic(root, &path, &present, NodeModulesState::Present).is_none()
153 );
154 assert!(
155 install_state_diagnostic(root, &path, &saved(None), NodeModulesState::Missing)
156 .is_none()
157 );
158 let diagnostic = install_state_diagnostic(root, &path, &present, NodeModulesState::Missing)
159 .expect("mismatch");
160 assert_eq!(
161 diagnostic.kind,
162 WorkspaceDiagnosticKind::BaselineInstallStateMismatch {
163 baseline_node_modules: NodeModulesState::Present,
164 run_node_modules: NodeModulesState::Missing,
165 }
166 );
167 assert!(diagnostic.degrades_analysis);
168 }
169}