diff --git a/libs/sqf/src/analyze/lints/s19_extra_not.rs b/libs/sqf/src/analyze/lints/s19_extra_not.rs new file mode 100644 index 00000000..c1b51547 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s19_extra_not.rs @@ -0,0 +1,134 @@ +use std::{ops::Range, sync::Arc}; + +use hemtt_common::config::LintConfig; +use hemtt_workspace::{ + lint::{AnyLintRunner, Lint, LintRunner}, + reporting::{Code, Codes, Diagnostic, Processed, Severity}, +}; + +use crate::{analyze::SqfLintData, Expression, UnaryCommand}; + +crate::analyze::lint!(LintS19ExtraNot); + +impl Lint for LintS19ExtraNot { + fn ident(&self) -> &'static str { + "extra_not" + } + + fn sort(&self) -> u32 { + 190 + } + + fn description(&self) -> &'static str { + "Checks for extra not before a comparison" + } + + fn documentation(&self) -> &'static str { + r"### Example + +**Incorrect** +```sqf +! (5 isEqualTo 6) +``` +**Correct** +```sqf +(5 isNotEqualTo 6) +``` +" + } + + fn default_config(&self) -> LintConfig { + LintConfig::help() + } + + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +struct Runner; +impl LintRunner for Runner { + type Target = crate::Expression; + + fn run( + &self, + _project: Option<&hemtt_common::config::ProjectConfig>, + config: &LintConfig, + processed: Option<&hemtt_workspace::reporting::Processed>, + target: &Self::Target, + _data: &SqfLintData, + ) -> Codes { + const COMPARISON_CMDS: &[&str] = &[ + "==", + "!=", + "isEqualTo", + "isNotEqualTo", + "<", + "<=", + ">", + ">=", + ]; + let Some(processed) = processed else { + return Vec::new(); + }; + let Expression::UnaryCommand(UnaryCommand::Not, rhs, range) = target else { + return Vec::new(); + }; + let Expression::BinaryCommand(ref last_cmd, _, _, _) = **rhs else { + return Vec::new(); + }; + if !COMPARISON_CMDS.contains(&last_cmd.as_str()) { + return Vec::new(); + } + + vec![Arc::new(Code19ExtraNot::new( + range.clone(), + processed, + config.severity(), + ))] + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct Code19ExtraNot { + span: Range, + severity: Severity, + diagnostic: Option, +} + +impl Code for Code19ExtraNot { + fn ident(&self) -> &'static str { + "L-S19" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#extra_not") + } + fn severity(&self) -> Severity { + self.severity + } + fn message(&self) -> String { + "Unneeded Not".to_string() + } + fn label_message(&self) -> String { + "unneeded not".to_string() + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl Code19ExtraNot { + #[must_use] + pub fn new(span: Range, processed: &Processed, severity: Severity) -> Self { + Self { + span, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/tests/lints.rs b/libs/sqf/tests/lints.rs index 01e91605..22f2683c 100644 --- a/libs/sqf/tests/lints.rs +++ b/libs/sqf/tests/lints.rs @@ -32,6 +32,7 @@ lint!(s09_banned_command); lint!(s11_if_not_else); lint!(s17_var_all_caps); lint!(s18_in_vehicle_check); +lint!(s19_extra_not); lint!(s20_bool_static_comparison); lint!(s21_invalid_comparisons); lint!(s22_this_call); diff --git a/libs/sqf/tests/lints/s19_extra_not.sqf b/libs/sqf/tests/lints/s19_extra_not.sqf new file mode 100644 index 00000000..a0eac2a3 --- /dev/null +++ b/libs/sqf/tests/lints/s19_extra_not.sqf @@ -0,0 +1 @@ +! (5 isEqualTo 6) diff --git a/libs/sqf/tests/snapshots/lints__simple_s19_extra_not.snap b/libs/sqf/tests/snapshots/lints__simple_s19_extra_not.snap new file mode 100644 index 00000000..af4d04ff --- /dev/null +++ b/libs/sqf/tests/snapshots/lints__simple_s19_extra_not.snap @@ -0,0 +1,9 @@ +--- +source: libs/sqf/tests/lints.rs +expression: lint(stringify! (s19_extra_not)) +--- +help[L-S19]: Unneeded Not + ┌─ s19_extra_not.sqf:1:1 + │ +1 │ ! (5 isEqualTo 6) + │ ^ unneeded not