diff --git a/CHANGELOG.md b/CHANGELOG.md index cf70f08..43d48a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed issue where statements under top-level in-eachs were not correctly tracked. - Moved storage of reference->symbol mapping to on-demand timing, should significantly speed up device analysises +- Unknown fields in the lint configuration file are now detected and reported as errors, helping users identify and correct typos or unsupported configuration options. - CLI tool DFA now uses default one-indexed line count for reporting warnings on analyzed files. `--zero-indexed` flag can be set to `true` when executing DFA for using zero-indexed counting if required. diff --git a/src/actions/mod.rs b/src/actions/mod.rs index cd9180e..8983546 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -342,17 +342,14 @@ impl InitActionContext { pid: u32, _client_supports_cmd_run: bool, ) -> InitActionContext { - let lint_config = Arc::new(Mutex::new( - config.lock().unwrap().lint_cfg_path.clone() - .and_then(maybe_parse_lint_cfg) - .unwrap_or_default())); + InitActionContext { vfs, analysis, analysis_queue: Arc::new(AnalysisQueue::init()), current_notifier: Arc::default(), config, - lint_config, + lint_config: Arc::new(Mutex::new(LintCfg::default())), jobs: Arc::default(), direct_opens: Arc::default(), quiescent: Arc::new(AtomicBool::new(false)), @@ -388,7 +385,8 @@ impl InitActionContext { fn init(&mut self, _init_options: InitializationOptions, out: O) { - self.update_compilation_info(&out) + self.update_compilation_info(&out); + self.update_linter_config(&out); } pub fn update_workspaces(&self, @@ -401,13 +399,17 @@ impl InitActionContext { } } - fn update_linter_config(&self, _out: &O) { + fn update_linter_config(&self, out: &O) { trace!("Updating linter config"); if let Ok(config) = self.config.lock() { - *self.lint_config.lock().unwrap() = - config.lint_cfg_path.clone() - .and_then(maybe_parse_lint_cfg) - .unwrap_or_default(); + if let Some(ref lint_path) = config.lint_cfg_path { + if let Some(cfg) = maybe_parse_lint_cfg(lint_path.clone(), out) { + *self.lint_config.lock().unwrap() = cfg; + } + } else { + // If no lint config path is set, use default + *self.lint_config.lock().unwrap() = LintCfg::default(); + } } } diff --git a/src/actions/notifications.rs b/src/actions/notifications.rs index 7dcb4e8..bb2be0d 100644 --- a/src/actions/notifications.rs +++ b/src/actions/notifications.rs @@ -249,6 +249,7 @@ impl BlockingNotificationAction for DidChangeWatchedFiles { if let Some(file_watch) = FileWatch::new(ctx) { if params.changes.iter().any(|c| file_watch.is_relevant(c)) { ctx.update_compilation_info(&out); + ctx.update_linter_config(&out); } } Ok(()) diff --git a/src/lint/mod.rs b/src/lint/mod.rs index 4ef2925..96b237d 100644 --- a/src/lint/mod.rs +++ b/src/lint/mod.rs @@ -22,19 +22,27 @@ use crate::lint::rules::indentation::{MAX_LENGTH_DEFAULT, INDENTATION_LEVEL_DEFAULT, setup_indentation_size }; +use crate::server::{maybe_notify_unknown_lint_fields, Output}; -pub fn parse_lint_cfg(path: PathBuf) -> Result { +pub fn parse_lint_cfg(path: PathBuf) -> Result<(LintCfg, Vec), String> { debug!("Reading Lint configuration from {:?}", path); - let file_content = fs::read_to_string(path).map_err( - |e|e.to_string())?; + let file_content = fs::read_to_string(path).map_err(|e| e.to_string())?; trace!("Content is {:?}", file_content); - serde_json::from_str(&file_content) - .map_err(|e|e.to_string()) + + let val: serde_json::Value = serde_json::from_str(&file_content) + .map_err(|e| e.to_string())?; + + let mut unknowns = Vec::new(); + let cfg = LintCfg::try_deserialize(&val, &mut unknowns)?; + + Ok((cfg, unknowns)) } -pub fn maybe_parse_lint_cfg(path: PathBuf) -> Option { +pub fn maybe_parse_lint_cfg(path: PathBuf, out: &O) -> Option { match parse_lint_cfg(path) { - Ok(mut cfg) => { + Ok((mut cfg, unknowns)) => { + // Send visible warning to client + maybe_notify_unknown_lint_fields(out, &unknowns); setup_indentation_size(&mut cfg); Some(cfg) }, @@ -45,9 +53,10 @@ pub fn maybe_parse_lint_cfg(path: PathBuf) -> Option { } } + + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(default)] -#[serde(deny_unknown_fields)] pub struct LintCfg { #[serde(default)] pub sp_brace: Option, @@ -81,6 +90,21 @@ pub struct LintCfg { pub annotate_lints: bool, } +impl LintCfg { + pub fn try_deserialize( + val: &serde_json::Value, + unknowns: &mut Vec, + ) -> Result { + // Use serde_ignored to automatically track unknown fields + match serde_ignored::deserialize(val, |json_field| { + unknowns.push(json_field.to_string()); + }) { + Ok(cfg) => Ok(cfg), + Err(e) => Err(e.to_string()), + } + } +} + fn get_true() -> bool { true } @@ -421,8 +445,47 @@ pub mod tests { let example_path = format!("{}{}", env!("CARGO_MANIFEST_DIR"), EXAMPLE_CFG); - let example_cfg = parse_lint_cfg(example_path.into()).unwrap(); + let (example_cfg, unknowns) = parse_lint_cfg(example_path.into()).unwrap(); assert_eq!(example_cfg, LintCfg::default()); + // Assert that there are no unknown fields in the example config: + assert!(unknowns.is_empty(), "Example config should not have unknown fields: {:?}", unknowns); + } + + #[test] + fn test_unknown_fields_detection() { + use crate::lint::LintCfg; + + // JSON with unknown fields + let json_with_unknowns = r#"{ + "sp_brace": {}, + "unknown_field_1": true, + "indent_size": {"indentation_spaces": 4}, + "another_unknown": "value" + }"#; + + let val: serde_json::Value = serde_json::from_str(json_with_unknowns).unwrap(); + let mut unknowns = Vec::new(); + let result = LintCfg::try_deserialize(&val, &mut unknowns); + + assert!(result.is_ok()); + let cfg = result.unwrap(); + + // Assert that unknown fields were detected + assert_eq!(unknowns.len(), 2); + assert!(unknowns.contains(&"unknown_field_1".to_string())); + assert!(unknowns.contains(&"another_unknown".to_string())); + + // Assert the final LintCfg matches expected json (the known fields) + let expected_json = r#"{ + "sp_brace": {}, + "indent_size": {"indentation_spaces": 4} + }"#; + let expected_val: serde_json::Value = serde_json::from_str(expected_json).unwrap(); + let mut expected_unknowns = Vec::new(); + let expected_cfg = LintCfg::try_deserialize(&expected_val, &mut expected_unknowns).unwrap(); + + assert_eq!(cfg, expected_cfg); + assert!(expected_unknowns.is_empty()); // No unknown fields in the expected config } #[test] diff --git a/src/server/mod.rs b/src/server/mod.rs index 95ba735..d2219a4 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -164,6 +164,22 @@ pub(crate) fn maybe_notify_deprecated_configs(out: &O, keys: &[String } } +pub(crate) fn maybe_notify_unknown_lint_fields(out: &O, unknowns: &[String]) { + if !unknowns.is_empty() { + let fields_list = unknowns.join(", "); + let message = format!( + "Unknown lint configuration field{}: {}. These will be ignored.", + if unknowns.len() > 1 { "s" } else { "" }, + fields_list + ); + + out.notify(Notification::::new(ShowMessageParams { + typ: MessageType::ERROR, + message, + })); + } +} + pub(crate) fn maybe_notify_duplicated_configs( out: &O, dups: &std::collections::HashMap>,