Skip to content
Merged
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
156 changes: 156 additions & 0 deletions libs/sqf/src/analyze/lints/s53_select_substring_len.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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::{
BinaryCommand::{self}, Expression, analyze::LintData,
};

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

impl Lint<LintData> for LintS53SelectSubstringLen {
fn ident(&self) -> &'static str {
"select_substring_len"
}
fn sort(&self) -> u32 {
530
}
fn description(&self) -> &'static str {
"Checks for substring length mismatch when using `select`"
}
fn documentation(&self) -> &'static str {
r#"### Example

**Incorrect**
```sqf
if (((currentWeapon player) select [0, 3]) == "ABC_") then {};
```

**Correct**
```sqf
if (((currentWeapon player) select [0, 4]) == "ABC_") then {};
```
"#
}
fn default_config(&self) -> LintConfig {
LintConfig::help()
}
fn runners(&self) -> Vec<Box<dyn AnyLintRunner<LintData>>> {
vec![Box::new(Runner)]
}
}

struct Runner;
impl LintRunner<LintData> for Runner {
type Target = crate::Expression;

fn run(
&self,
_project: Option<&hemtt_common::config::ProjectConfig>,
config: &LintConfig,
processed: Option<&hemtt_workspace::reporting::Processed>,
_runtime: &hemtt_common::config::RuntimeArguments,
target: &Self::Target,
_data: &LintData,
) -> Codes {
fn match_pair(e1: &Expression, e2: &Expression) -> Option<(usize, usize)> {
let Expression::String(str, _, _) = e1 else {
return None;
};
let Expression::BinaryCommand(BinaryCommand::Named(cmd), _sel_lhs, sel_rhs, _) = e2 else {
return None;
};
if !cmd.eq_ignore_ascii_case("select") {
return None;
}
let Expression::Array(arr, _) = sel_rhs.as_ref() else {
return None;
};
if arr.len() != 2 {
return None;
}
let Expression::Number(sel_len, _) = arr[1] else {
return None;
};
let str_len = str.len();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let sel_len = sel_len.0.round() as usize;
if str_len == sel_len {
return None;
}
Some((str_len, sel_len))
}

let Some(processed) = processed else {
return Vec::new();
};
let Expression::BinaryCommand(bcmd, lhs, rhs, span) = target else {
return Vec::new();
};
if !(bcmd == &BinaryCommand::Eq || bcmd == &BinaryCommand::NotEq || bcmd.as_str().eq_ignore_ascii_case("isEqualTo") || bcmd.as_str().eq_ignore_ascii_case("isNotEqualTo")) {
return Vec::new();
}
let len_pair = match_pair(lhs, rhs).or_else(|| match_pair(rhs, lhs));
let Some((str_len, sel_len)) = len_pair else {
return Vec::new();
};
vec![Arc::new(CodeS53SelectSubstringLen::new(
span.clone(),
str_len, sel_len,
config.severity(),
processed,
))]
}
}

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

impl Code for CodeS53SelectSubstringLen {
fn ident(&self) -> &'static str {
"L-S53"
}
fn link(&self) -> Option<&str> {
Some("/lints/sqf.html#select_substring_len")
}
fn severity(&self) -> Severity {
self.severity
}
fn message(&self) -> String {
"Select substring length does not match string length".to_string()
}
fn label_message(&self) -> String {
format!("substring is length {} | string is length {}", self.sel_len, self.str_len)
}
fn diagnostic(&self) -> Option<Diagnostic> {
self.diagnostic.clone()
}
}

impl CodeS53SelectSubstringLen {
#[must_use]
pub fn new(span: Range<usize>, str_len: usize, sel_len: usize, severity: Severity, processed: &Processed) -> Self {
Self {
span,
str_len,
sel_len,
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
}
}
1 change: 1 addition & 0 deletions libs/sqf/tests/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ lint!(s49_count_type, true);
lint!(s50_count_side, true);
lint!(s51_push_back_unique, true);
lint!(s52_duplicate_case, true);
lint!(s53_select_substring_len, true);

#[test]
fn test_s29_function_undefined() {
Expand Down
7 changes: 7 additions & 0 deletions libs/sqf/tests/lints/s53_select_substring_len.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if (((currentWeapon player) select [0, 3]) == "ABC_") then {};
if (x1 select [0, 3] isNotEqualTo "CasE") then {};
"prefix" == addon_name select [0, 999];


x1 select [0, 3] == "ABC"; // fine
x2 select [3] == "something"; // fine
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: libs/sqf/tests/lints.rs
expression: "lint(stringify! (s53_select_substring_len), true).0"
---
help[L-S53]: Select substring length does not match string length
┌─ s53_select_substring_len.sqf:1:44
│
1 │ if (((currentWeapon player) select [0, 3]) == "ABC_") then {};
│ ^^ substring is length 3 | string is length 4


help[L-S53]: Select substring length does not match string length
┌─ s53_select_substring_len.sqf:2:22
│
2 │ if (x1 select [0, 3] isNotEqualTo "CasE") then {};
│ ^^^^^^^^^^^^ substring is length 3 | string is length 4


help[L-S53]: Select substring length does not match string length
┌─ s53_select_substring_len.sqf:3:10
│
3 │ "prefix" == addon_name select [0, 999];
│ ^^ substring is length 999 | string is length 6