Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sqf: Add lint (S25) for count array comparisons #859

Open
wants to merge 1 commit into
base: inspector
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion libs/sqf/src/analyze/inspector/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

use std::{collections::HashSet, ops::Range};

use crate::{analyze::inspector::VarSource, parser::database::Database, Expression};
use crate::{
analyze::inspector::{Issue, VarSource},
parser::database::Database,
Expression,
};

use super::{game_value::GameValue, SciptScope};

Expand Down Expand Up @@ -403,4 +407,35 @@ impl SciptScope {
}
return_value
}

pub fn cmd_eqx_count_lint(
&mut self,
lhs: &Box<Expression>,
rhs: &Box<Expression>,
source: &Range<usize>,
database: &Database,
equal_zero: bool,
) {
let Expression::Number(float_ord::FloatOrd(0.0), _) = **rhs else {
return;
};
let Expression::UnaryCommand(crate::UnaryCommand::Named(ref lhs_cmd), ref count_input, _) =
**lhs
else {
return;
};
if lhs_cmd != "count" {
return;
}
let count_input_set = self.eval_expression(count_input, database);
if count_input_set.is_empty()
|| !count_input_set
.iter()
.all(|arr| matches!(arr, GameValue::Array(_)))
{
return;
}
self.errors
.insert(Issue::CountArrayComparison(equal_zero, source.clone()));
}
}
9 changes: 9 additions & 0 deletions libs/sqf/src/analyze/inspector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub enum Issue {
Unused(String, VarSource),
Shadowed(String, Range<usize>),
NotPrivate(String, Range<usize>),
CountArrayComparison(bool, Range<usize>),
}

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
Expand Down Expand Up @@ -317,6 +318,14 @@ impl SciptScope {
None
}
BinaryCommand::Else => Some(self.cmd_b_else(&lhs_set, &rhs_set)),
BinaryCommand::Eq => {
self.cmd_eqx_count_lint(&lhs, &rhs, source, database, true);
None
}
BinaryCommand::Greater | BinaryCommand::NotEq => {
self.cmd_eqx_count_lint(&lhs, &rhs, source, database, false);
None
}
BinaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() {
"params" => Some(self.cmd_generic_params(&rhs_set)),
"call" => {
Expand Down
4 changes: 2 additions & 2 deletions libs/sqf/src/analyze/lints/s21_invalid_comparisons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ impl LintRunner<LintData> for Runner {
return Vec::new();
}

let comparisions = extract_comparisons(arg);
let flat = flatten_comparisons(comparisions);
let comparisons = extract_comparisons(arg);
let flat = flatten_comparisons(comparisons);
let issues = find_issues(flat);
issues
.into_iter()
Expand Down
135 changes: 135 additions & 0 deletions libs/sqf/src/analyze/lints/s25_count_array_comp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::{
analyze::{inspector::Issue, LintData},
Statements,
};
use hemtt_common::config::LintConfig;
use hemtt_workspace::{
lint::{AnyLintRunner, Lint, LintRunner},
reporting::{Code, Codes, Diagnostic, Processed, Severity},
};
use std::{ops::Range, sync::Arc};

crate::analyze::lint!(LintS25CountArrayComparison);

impl Lint<LintData> for LintS25CountArrayComparison {
fn ident(&self) -> &'static str {
"count_array_comp"
}
fn sort(&self) -> u32 {
250
}
fn description(&self) -> &'static str {
"Count Array Comp"
}
fn documentation(&self) -> &'static str {
r"### Example

**Incorrect**
```sqf
count [] == 0
```

### Explanation

Checks for unoptimized `count array` checks."
}
fn default_config(&self) -> LintConfig {
LintConfig::help().with_enabled(true)
}
fn runners(&self) -> Vec<Box<dyn AnyLintRunner<LintData>>> {
vec![Box::new(Runner)]
}
}

pub struct Runner;
impl LintRunner<LintData> for Runner {
type Target = Statements;
fn run(
&self,
_project: Option<&hemtt_common::config::ProjectConfig>,
config: &hemtt_common::config::LintConfig,
processed: Option<&hemtt_workspace::reporting::Processed>,
target: &Statements,
_data: &LintData,
) -> hemtt_workspace::reporting::Codes {
if target.issues().is_empty() {
return Vec::new();
};
let Some(processed) = processed else {
return Vec::new();
};
let mut errors: Codes = Vec::new();
for issue in target.issues() {
if let Issue::CountArrayComparison(equal_zero, range) = issue {
errors.push(Arc::new(CodeS25CountArrayComp::new(
range.to_owned(),
equal_zero.to_owned(),
config.severity(),
processed,
)));
}
}
errors
}
}

#[allow(clippy::module_name_repetitions)]
pub struct CodeS25CountArrayComp {
span: Range<usize>,
equal_zero: bool,
severity: Severity,
diagnostic: Option<Diagnostic>,
}

impl Code for CodeS25CountArrayComp {
fn ident(&self) -> &'static str {
"L-S25"
}
fn link(&self) -> Option<&str> {
Some("/analysis/sqf.html#count_array_comp")
}
/// Top message
fn message(&self) -> String {
format!("count array comparison")
}
/// Under ^^^span hint
fn label_message(&self) -> String {
String::new()
}
/// bottom note
fn note(&self) -> Option<String> {
if self.equal_zero {
Some("use `isEqualTo []`".into())
} else {
Some("use `isNotEqualTo []`".into())
}
}
fn severity(&self) -> Severity {
self.severity
}
fn diagnostic(&self) -> Option<Diagnostic> {
self.diagnostic.clone()
}
}

impl CodeS25CountArrayComp {
#[must_use]
pub fn new(
span: Range<usize>,
equal_zero: bool,
severity: Severity,
processed: &Processed,
) -> Self {
Self {
span,
equal_zero,
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
}
}
5 changes: 4 additions & 1 deletion libs/sqf/tests/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mod tests {
pub fn test_1() {
let (_pro, sqf, _database) = get_statements("test_1.sqf");
let result = sqf.issues();
assert_eq!(result.len(), 15);
assert_eq!(result.len(), 16);
// Order not guarenteed
assert!(result.iter().any(|i| {
if let Issue::InvalidArgs(cmd, _) = i {
Expand Down Expand Up @@ -144,6 +144,9 @@ mod tests {
false
}
}));
assert!(result
.iter()
.any(|i| { matches!(i, Issue::CountArrayComparison(true, _)) }));
}

#[test]
Expand Down
2 changes: 2 additions & 0 deletions libs/sqf/tests/inspector/test_1.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,5 @@ format _varP;
[_test13] call some_func; // undef, is orphan
}, player] call unknown_fnc_Usage;

private _test14 = str 12345678 splitString "5";
if (count _test14 == 0) then { call b };
1 change: 1 addition & 0 deletions libs/sqf/tests/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ lint!(s21_invalid_comparisons, true);
lint!(s22_this_call, true);
lint!(s23_reassign_reserved_variable, true);
lint!(s24_marker_spam, true);
lint!(s25_count_array_comp, false);

fn lint(file: &str, ignore_inspector: bool) -> String {
let folder = std::path::PathBuf::from(ROOT);
Expand Down
2 changes: 2 additions & 0 deletions libs/sqf/tests/lints/s25_count_array_comp.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
private _z = [];
if (count _z == 0) then { call b };
11 changes: 11 additions & 0 deletions libs/sqf/tests/snapshots/lints__simple_s25_count_array_comp.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: libs/sqf/tests/lints.rs
expression: "lint(stringify! (s25_count_array_comp), false)"
---
help[L-S25]: count array comparison
┌─ s25_count_array_comp.sqf:2:14
│
2 │ if (count _z == 0) then { call b };
│ ^^
│
= note: use `isEqualTo []`
Loading