From 7f1ec1df46d88edadd233d01c9c0dfa7d10425a7 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 25 Sep 2024 20:03:38 -0500 Subject: [PATCH 01/24] sqf: Inspector Initial --- Cargo.lock | 1 + libs/sqf/Cargo.toml | 1 + libs/sqf/src/analyze/inspector/game_value.rs | 237 ++++++ libs/sqf/src/analyze/inspector/mod.rs | 739 ++++++++++++++++++ libs/sqf/src/analyze/lints/s12_inspector.rs | 252 ++++++ libs/sqf/src/analyze/mod.rs | 2 + libs/sqf/src/lib.rs | 1 + libs/sqf/src/parser/mod.rs | 2 + libs/sqf/tests/inspector.rs | 105 +++ libs/sqf/tests/inspector/test_0.sqf | 0 libs/sqf/tests/inspector/test_1.sqf | 48 ++ libs/sqf/tests/lints.rs | 1 + libs/sqf/tests/lints/project_tests.toml | 3 + .../tests/lints/s06_find_in_str/source.sqf | 2 +- .../tests/lints/s06_find_in_str/stdout.ansi | 6 +- libs/sqf/tests/lints/s12_inspector/source.sqf | 4 + .../sqf/tests/lints/s12_inspector/stdout.ansi | 13 + 17 files changed, 1413 insertions(+), 4 deletions(-) create mode 100644 libs/sqf/src/analyze/inspector/game_value.rs create mode 100644 libs/sqf/src/analyze/inspector/mod.rs create mode 100644 libs/sqf/src/analyze/lints/s12_inspector.rs create mode 100644 libs/sqf/tests/inspector.rs create mode 100644 libs/sqf/tests/inspector/test_0.sqf create mode 100644 libs/sqf/tests/inspector/test_1.sqf create mode 100644 libs/sqf/tests/lints/s12_inspector/source.sqf create mode 100644 libs/sqf/tests/lints/s12_inspector/stdout.ansi diff --git a/Cargo.lock b/Cargo.lock index c87fe04bd..c21014d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,6 +1553,7 @@ dependencies = [ "hemtt-workspace", "linkme", "paste", + "regex", "toml 0.8.19", "tracing", ] diff --git a/libs/sqf/Cargo.toml b/libs/sqf/Cargo.toml index ae9872b71..5ff95a981 100644 --- a/libs/sqf/Cargo.toml +++ b/libs/sqf/Cargo.toml @@ -24,6 +24,7 @@ float-ord = "0.3.2" linkme = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +regex = { workspace = true } [features] default = ["compiler", "parser"] diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs new file mode 100644 index 000000000..d5a10407f --- /dev/null +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -0,0 +1,237 @@ +use std::{collections::HashSet, sync::Arc}; + +use arma3_wiki::model::{Arg, Call, Param, Value}; +use tracing::{trace, warn}; + +use crate::{parser::database::Database, Expression}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum GameValue { + Anything, + // Assignment, // as in z = call {x=1}??? + Array(Option), + Boolean(Option), + Code(Option), + Config, + Control, + DiaryRecord, + Display, + ForType(Option), + Group, + HashMap, + IfType, + Location, + Namespace, + Number(Option), + Nothing, + Object, + ScriptHandle, + Side, + String(Option), + SwitchType, + Task, + TeamMember, + WhileType, + WithType, +} + +impl GameValue { + #[must_use] + pub fn from_cmd( + expression: &Expression, + lhs_set: Option<&HashSet>, + rhs_set: Option<&HashSet>, + database: &Arc, + ) -> HashSet { + let mut return_types = HashSet::new(); + let cmd_name = expression.command_name().expect("has a name"); + let Some(command) = database.wiki().commands().get(cmd_name) else { + trace!("cmd {cmd_name} not in db?"); //ToDo: this can't find short cmds like &&, || + return HashSet::from([Self::Anything]); + }; + + for syntax in command.syntax() { + match syntax.call() { + Call::Nular => { + if !matches!(expression, Expression::NularCommand(..)) { + continue; + } + } + Call::Unary(rhs_arg) => { + if !matches!(expression, Expression::UnaryCommand(..)) + || !Self::match_set_to_arg( + rhs_set.expect("u args"), + rhs_arg, + syntax.params(), + ) + { + continue; + } + } + Call::Binary(lhs_arg, rhs_arg) => { + if !matches!(expression, Expression::BinaryCommand(..)) + || !Self::match_set_to_arg( + lhs_set.expect("b args"), + lhs_arg, + syntax.params(), + ) + || !Self::match_set_to_arg( + rhs_set.expect("b args"), + rhs_arg, + syntax.params(), + ) + { + continue; + } + } + } + let value = &syntax.ret().0; + return_types.insert(Self::from_wiki_value(value)); + } + // trace!( + // "cmd [{}] = {}:{:?}", + // cmd_name, + // return_types.len(), + // return_types + // ); + return_types + } + + #[must_use] + pub fn match_set_to_arg(set: &HashSet, arg: &Arg, params: &[Param]) -> bool { + match arg { + Arg::Item(name) => { + // trace!("looking for {name} in {params:?}"); + let Some(param) = params.iter().find(|p| p.name() == name) else { + warn!("param not found"); + return true; + }; + Self::match_set_to_value(set, param.typ()) + } + Arg::Array(_vec_arg) => { + // todo: each individual array arg + Self::match_set_to_value(set, &Value::ArrayUnknown) + } + } + } + + #[must_use] + pub fn match_set_to_value(set: &HashSet, right_wiki: &Value) -> bool { + let right = Self::from_wiki_value(right_wiki); + set.iter().any(|gv| Self::match_values(gv, &right)) + } + + #[must_use] + /// matches values are compatible (Anything will always match) + /// todo: think about how nil and any interact? + pub fn match_values(left: &Self, right: &Self) -> bool { + if matches!(left, Self::Anything) { + return true; + } + if matches!(right, Self::Anything) { + return true; + } + std::mem::discriminant(left) == std::mem::discriminant(right) + } + + #[must_use] + /// Maps from Wiki:Value to Inspector:GameValue + pub fn from_wiki_value(value: &Value) -> Self { + match value { + Value::Anything => Self::Anything, + Value::ArrayColor + | Value::ArrayColorRgb + | Value::ArrayColorRgba + | Value::ArrayDate + | Value::ArraySized { .. } + | Value::ArrayUnknown + | Value::ArrayUnsized { .. } + | Value::Position + | Value::Waypoint => Self::Array(None), + Value::Boolean => Self::Boolean(None), + Value::Code => Self::Code(None), + Value::Config => Self::Config, + Value::Control => Self::Control, + Value::DiaryRecord => Self::DiaryRecord, + Value::Display => Self::Display, + Value::ForType => Self::ForType(None), + Value::IfType => Self::IfType, + Value::Group => Self::Group, + Value::Location => Self::Location, + Value::Namespace => Self::Namespace, + Value::Nothing => Self::Nothing, + Value::Number => Self::Number(None), + Value::Object => Self::Object, + Value::ScriptHandle => Self::ScriptHandle, + Value::Side => Self::Side, + Value::String => Self::String(None), + Value::SwitchType => Self::SwitchType, + Value::Task => Self::Task, + Value::TeamMember => Self::TeamMember, + Value::WhileType => Self::WhileType, + Value::WithType => Self::WithType, + Value::Unknown => { + trace!("wiki has syntax with [unknown] type"); + Self::Anything + } + _ => { + warn!("wiki type [{value:?}] not matched"); + Self::Anything + } + } + } + + #[must_use] + /// Get as a string for debugging + pub fn as_debug(&self) -> String { + match self { + // Self::Assignment() => { + // format!("Assignment") + // } + Self::Anything => "Anything".to_string(), + Self::ForType(expression) => { + if let Some(Expression::String(str, _, _)) = expression { + format!("ForType(var {str})") + } else { + "ForType(GENERIC)".to_string() + } + } + Self::Number(expression) => { + if let Some(Expression::Number(num, _)) = expression { + format!("Number({num:?})",) + } else { + "Number(GENERIC)".to_string() + } + } + Self::String(expression) => { + if let Some(Expression::String(str, _, _)) = expression { + format!("String({str})") + } else { + "String(GENERIC)".to_string() + } + } + Self::Boolean(expression) => { + if let Some(Expression::Boolean(bool, _)) = expression { + format!("Boolean({bool})") + } else { + "Boolean(GENERIC)".to_string() + } + } + Self::Array(expression) => { + if let Some(Expression::Array(array, _)) = expression { + format!("ArrayExp(len {})", array.len()) + } else { + "ArrayExp(GENERIC)".to_string() + } + } + Self::Code(expression) => { + if let Some(Expression::Code(statements)) = expression { + format!("Code(len {})", statements.content().len()) + } else { + "Code(GENERIC)".to_string() + } + } + _ => "Other(todo)".to_string(), + } + } +} diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs new file mode 100644 index 000000000..0ea2c936a --- /dev/null +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -0,0 +1,739 @@ +//! Inspects code, checking code args and variable usage +//! +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + ops::Range, + sync::Arc, + vec, +}; + +use crate::{ + parser::database::Database, BinaryCommand, Expression, Statement, Statements, UnaryCommand, +}; +use game_value::GameValue; +use hemtt_workspace::reporting::Processed; +use regex::Regex; +use tracing::{error, trace}; + +mod game_value; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Issue { + InvalidArgs(String, Range), + Undefined(String, Range, bool), + Unused(String, Range), + Shadowed(String, Range), + NotPrivate(String, Range), +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum VarSource { + Real(Range), + Magic(Range), + Ignore, +} +impl VarSource { + #[must_use] + pub const fn check_unused(&self) -> bool { + matches!(self, Self::Real(..)) + } + #[must_use] + pub const fn check_shadow(&self) -> bool { + matches!(self, Self::Real(..)) + } + #[must_use] + pub fn get_range(&self) -> Option> { + match self { + Self::Real(range) | Self::Magic(range) => Some(range.clone()), + Self::Ignore => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VarHolder { + possible: HashSet, + usage: i32, + source: VarSource, +} + +pub type Stack = HashMap; + +pub struct SciptScope { + database: Arc, + errors: HashSet, + global: Stack, + local: Vec, + code_seen: HashSet, + code_used: HashSet, + is_child: bool, + ignored_vars: HashSet, +} + +impl SciptScope { + #[must_use] + pub fn create( + ignored_vars: &HashSet, + database: &Arc, + is_child: bool, + ) -> Self { + // trace!("Creating ScriptScope"); + let mut scope = Self { + database: database.clone(), + errors: HashSet::new(), + global: Stack::new(), + local: Vec::new(), + code_seen: HashSet::new(), + code_used: HashSet::new(), + is_child, + ignored_vars: ignored_vars.clone(), + }; + scope.push(); + for var in ignored_vars { + scope.var_assign( + var, + true, + HashSet::from([GameValue::Anything]), + VarSource::Ignore, + ); + } + scope + } + #[must_use] + pub fn finish(&mut self, check_child_scripts: bool) -> HashSet { + self.pop(); + if check_child_scripts { + let unused = &self.code_seen - &self.code_used; + for expression in unused { + let Expression::Code(statements) = expression else { + error!("non-code in unused"); + continue; + }; + // trace!("-- Checking external scope"); + let mut external_scope = Self::create(&self.ignored_vars, &self.database, true); + external_scope.eval_statements(&statements); + self.errors + .extend(external_scope.finish(check_child_scripts)); + } + } + self.errors.clone() + } + + pub fn push(&mut self) { + // trace!("-- Stack Push {}", self.local.len()); + self.local.push(Stack::new()); + } + pub fn pop(&mut self) { + for (var, holder) in self.local.pop().unwrap_or_default() { + // trace!("-- Stack Pop {}:{} ", var, holder.usage); + if holder.usage == 0 && holder.source.check_unused() { + self.errors.insert(Issue::Unused( + var, + holder.source.get_range().unwrap_or_default(), + )); + } + } + } + + pub fn var_assign( + &mut self, + var: &str, + local: bool, + possible_values: HashSet, + source: VarSource, + ) { + trace!("var_assign: {} @ {}", var, self.local.len()); + let var_lower = var.to_ascii_lowercase(); + if !var_lower.starts_with('_') { + let holder = self.global.entry(var_lower).or_insert(VarHolder { + possible: HashSet::new(), + usage: 0, + source, + }); + holder.possible.extend(possible_values); + return; + } + + let stack_level_search = self + .local + .iter() + .rev() + .position(|s| s.contains_key(&var_lower)); + let mut stack_level = self.local.len() - 1; + if stack_level_search.is_none() { + if !local { + self.errors.insert(Issue::NotPrivate( + var.to_owned(), + source.get_range().unwrap_or_default(), + )); + } + } else if local { + if source.check_shadow() { + self.errors.insert(Issue::Shadowed( + var.to_owned(), + source.get_range().unwrap_or_default(), + )); + } + } else { + stack_level -= stack_level_search.unwrap_or_default(); + } + let holder = self.local[stack_level] + .entry(var_lower) + .or_insert(VarHolder { + possible: HashSet::new(), + usage: 0, + source, + }); + holder.possible.extend(possible_values); + } + + #[must_use] + /// # Panics + pub fn var_retrieve( + &mut self, + var: &str, + source: &Range, + peek: bool, + ) -> HashSet { + let var_lower = var.to_ascii_lowercase(); + let holder_option = if var_lower.starts_with('_') { + let stack_level_search = self + .local + .iter() + .rev() + .position(|s| s.contains_key(&var_lower)); + let mut stack_level = self.local.len() - 1; + if stack_level_search.is_none() { + if !peek { + self.errors.insert(Issue::Undefined( + var.to_owned(), + source.clone(), + self.is_child, + )); + } + } else { + stack_level -= stack_level_search.expect("is_some"); + }; + self.local[stack_level].get_mut(&var_lower) + } else if self.global.contains_key(&var_lower) { + self.global.get_mut(&var_lower) + } else { + return HashSet::from([GameValue::Anything]); + }; + if holder_option.is_none() { + // we've reported the error above, just return Any so it doesn't fail everything after + HashSet::from([GameValue::Anything]) + } else { + let holder = holder_option.expect("is_some"); + holder.usage += 1; + let mut set = holder.possible.clone(); + + if !var_lower.starts_with('_') && self.ignored_vars.contains(&var.to_ascii_lowercase()) + { + // Assume that a ignored global var could be anything + set.insert(GameValue::Anything); + } + set + } + } + #[must_use] + pub fn cmd_u_private(&mut self, rhs: &HashSet) -> HashSet { + fn push_var(s: &mut SciptScope, var: &String, source: &Range) { + if s.ignored_vars.contains(&var.to_ascii_lowercase()) { + s.var_assign( + &var.to_string(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Ignore, + ); + } else { + s.var_assign( + &var.to_string(), + true, + HashSet::from([GameValue::Nothing]), + VarSource::Real(source.clone()), + ); + } + } + for possible in rhs { + if let GameValue::Array(Some(Expression::Array(array, _))) = possible { + for element in array { + let Expression::String(var, source, _) = element else { + continue; + }; + if var.is_empty() { + continue; + } + push_var(self, &var.to_string(), source); + } + } + if let GameValue::String(Some(Expression::String(var, source, _))) = possible { + if var.is_empty() { + continue; + } + push_var(self, &var.to_string(), source); + } + } + HashSet::new() + } + #[must_use] + pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + continue; + }; + + for entry in array { + match entry { + Expression::String(var, source, _) => { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Real(source.clone()), + ); + } + Expression::Array(var_array, _) => { + if !var_array.is_empty() { + if let Expression::String(var, source, _) = &var_array[0] { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Real(source.clone()), + ); + } + } + } + _ => {} + } + } + } + HashSet::from([GameValue::Boolean(None)]) + } + #[must_use] + pub fn cmd_generic_call(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + self.code_used.insert(expression.clone()); + self.eval_statements(statements); + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_b_do( + &mut self, + lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + // look for forType vars with valid strings (ignore old style code) + let mut do_run = true; + for possible in lhs { + if let GameValue::ForType(option) = possible { + match option { + Some(Expression::String(var, source, _)) => { + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Number(None)]), + VarSource::Real(source.clone()), + ); + } + Some(Expression::Array(array, _)) => { + if array.len() != 3 { + error!("for wrong len"); + continue; + } + for for_stage in array { + let Expression::Code(for_statements) = for_stage else { + continue; + }; + self.code_used.insert(for_stage.clone()); + self.eval_statements(for_statements); + } + } + None => { + do_run = false; + } + _ => { + unreachable!(""); + } + } + } + } + self.code_used.insert(expression.clone()); + if do_run { + self.eval_statements(statements); + } + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_generic_call_magic( + &mut self, + code_possibilities: &HashSet, + magic: &Vec, + source: &Range, + ) -> HashSet { + for possible in code_possibilities { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + for var in magic { + self.var_assign( + var, + true, + HashSet::from([GameValue::Anything]), + VarSource::Magic(source.clone()), + ); + } + self.code_used.insert(expression.clone()); + self.eval_statements(statements); + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_for(&mut self, rhs: &HashSet) -> HashSet { + let mut return_value = HashSet::new(); + for possible in rhs { + match possible { + GameValue::Array(option) | GameValue::String(option) => { + return_value.insert(GameValue::ForType(option.clone())); + } + _ => { + error!("shouldn't be reachable?"); + return_value.insert(GameValue::ForType(None)); + } + } + } + return_value + } + #[must_use] + /// for (from, to, step) chained commands + pub fn cmd_b_from_chain( + &self, + lhs: &HashSet, + _rhs: &HashSet, + ) -> HashSet { + lhs.clone() + } + #[must_use] + pub fn cmd_u_is_nil(&mut self, rhs: &HashSet) -> HashSet { + let mut non_string = false; + for possible in rhs { + let GameValue::String(possible) = possible else { + non_string = true; + continue; + }; + let Some(expression) = possible else { + continue; + }; + let Expression::String(var, _, _) = expression else { + continue; + }; + let _ = self.var_retrieve(var, &expression.span(), true); + } + if non_string { + let _ = self.cmd_generic_call(rhs); + } + HashSet::from([GameValue::Boolean(None)]) + } + #[must_use] + pub fn cmd_b_then( + &mut self, + _lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + let mut return_value = HashSet::new(); + for possible in rhs { + if let GameValue::Code(Some(Expression::Code(_statements))) = possible { + return_value.extend(self.cmd_generic_call(rhs)); + } + if let GameValue::Array(Some(Expression::Array(array, _))) = possible { + for expression in array { + return_value.extend(self.cmd_generic_call(&HashSet::from([GameValue::Code( + Some(expression.clone()), + )]))); + } + } + } + return_value + } + #[must_use] + pub fn cmd_b_else( + &self, + lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + let mut return_value = HashSet::new(); // just merge, not really the same but should be fine + for possible in rhs { + return_value.insert(possible.clone()); + } + for possible in lhs { + return_value.insert(possible.clone()); + } + return_value + } + #[must_use] + pub fn cmd_b_get_or_default_call(&mut self, rhs: &HashSet) -> HashSet { + let mut possible_code = HashSet::new(); + for possible in rhs { + let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + continue; + }; + if array.len() < 2 { + continue; + } + possible_code.insert(GameValue::Code(Some(array[1].clone()))); + } + let _ = self.cmd_generic_call(&possible_code); + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_u_to_string(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(_) = expression else { + continue; + }; + // just skip because it will often use a _x + self.code_used.insert(expression.clone()); + } + HashSet::from([GameValue::String(None)]) + } + + #[must_use] + #[allow(clippy::too_many_lines)] + /// Evaluate expression in current scope + pub fn eval_expression(&mut self, expression: &Expression) -> HashSet { + let mut debug_type = String::new(); + let possible_values = match expression { + Expression::Variable(var, source) => self.var_retrieve(var, source, false), + Expression::Number(..) => HashSet::from([GameValue::Number(Some(expression.clone()))]), + Expression::Boolean(..) => { + HashSet::from([GameValue::Boolean(Some(expression.clone()))]) + } + Expression::String(..) => HashSet::from([GameValue::String(Some(expression.clone()))]), + Expression::Array(array, _) => { + for e in array { + let _ = self.eval_expression(e); + } + HashSet::from([GameValue::Array(Some(expression.clone()))]) + } + Expression::NularCommand(cmd, source) => { + debug_type = format!("[N:{}]", cmd.as_str()); + let cmd_set = GameValue::from_cmd(expression, None, None, &self.database); + if cmd_set.is_empty() { + // is this possible? + self.errors + .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + } + cmd_set + } + Expression::UnaryCommand(cmd, rhs, source) => { + debug_type = format!("[U:{}]", cmd.as_str()); + let rhs_set = self.eval_expression(rhs); + let cmd_set = GameValue::from_cmd(expression, None, Some(&rhs_set), &self.database); + if cmd_set.is_empty() { + self.errors + .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + } + let return_set = match cmd { + UnaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { + "params" => Some(self.cmd_generic_params(&rhs_set)), + "private" => Some(self.cmd_u_private(&rhs_set)), + "call" => Some(self.cmd_generic_call(&rhs_set)), + "isnil" => Some(self.cmd_u_is_nil(&rhs_set)), + "while" | "waituntil" | "default" => { + let _ = self.cmd_generic_call(&rhs_set); + None + } + "for" => Some(self.cmd_for(&rhs_set)), + "tostring" => Some(self.cmd_u_to_string(&rhs_set)), + _ => None, + }, + _ => None, + }; + // Use custom return from cmd or just use wiki set + return_set.unwrap_or(cmd_set) + } + Expression::BinaryCommand(cmd, lhs, rhs, source) => { + debug_type = format!("[B:{}]", cmd.as_str()); + let lhs_set = self.eval_expression(lhs); + let rhs_set = self.eval_expression(rhs); + let cmd_set = + GameValue::from_cmd(expression, Some(&lhs_set), Some(&rhs_set), &self.database); + if cmd_set.is_empty() { + // we must have invalid args + self.errors + .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + } + let return_set = match cmd { + BinaryCommand::Associate => { + // the : from case + let _ = self.cmd_generic_call(&rhs_set); + None + } + BinaryCommand::And | BinaryCommand::Or => { + let _ = self.cmd_generic_call(&rhs_set); + None + } + BinaryCommand::Else => Some(self.cmd_b_else(&lhs_set, &rhs_set)), + BinaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { + "params" => Some(self.cmd_generic_params(&rhs_set)), + "call" => Some(self.cmd_generic_call(&rhs_set)), + "exitwith" => { + // todo: handle scope exits + Some(self.cmd_generic_call(&rhs_set)) + } + "do" => { + // from While, With, For, and Switch + Some(self.cmd_b_do(&lhs_set, &rhs_set)) + } + "from" | "to" | "step" => Some(self.cmd_b_from_chain(&lhs_set, &rhs_set)), + "then" => Some(self.cmd_b_then(&lhs_set, &rhs_set)), + "foreach" | "foreachreversed" => Some(self.cmd_generic_call_magic( + &lhs_set, + &vec![ + "_x".to_string(), + "_y".to_string(), + "_forEachIndex".to_string(), + ], + source, + )), + "count" => { + let _ = self.cmd_generic_call_magic( + &lhs_set, + &vec!["_x".to_string()], + source, + ); + None + } + "findif" | "apply" | "select" => { + //todo handle (array select number) or (string select [1,2]); + let _ = self.cmd_generic_call_magic( + &rhs_set, + &vec!["_x".to_string()], + source, + ); + None + } + "getordefaultcall" => Some(self.cmd_b_get_or_default_call(&rhs_set)), + _ => None, + }, + _ => None, + }; + // Use custom return from cmd or just use wiki set + return_set.unwrap_or(cmd_set) + } + Expression::Code(statements) => { + self.code_seen.insert(expression.clone()); + debug_type = format!("CODE:{}", statements.content().len()); + HashSet::from([GameValue::Code(Some(expression.clone()))]) + } + Expression::ConsumeableArray(_, _) => unreachable!(""), + }; + trace!( + "eval expression{}->{:?}", + debug_type, + possible_values + .iter() + .map(GameValue::as_debug) + .collect::>() + ); + possible_values + } + + /// Evaluate statements in the current scope + fn eval_statements(&mut self, statements: &Statements) { + // let mut return_value = HashSet::new(); + for statement in statements.content() { + match statement { + Statement::AssignGlobal(var, expression, source) => { + // x or _x + let possible_values = self.eval_expression(expression); + self.var_assign(var, false, possible_values, VarSource::Real(source.clone())); + // return_value = vec![GameValue::Assignment()]; + } + Statement::AssignLocal(var, expression, source) => { + // private _x + let possible_values = self.eval_expression(expression); + self.var_assign(var, true, possible_values, VarSource::Real(source.clone())); + // return_value = vec![GameValue::Assignment()]; + } + Statement::Expression(expression, _) => { + let _possible_values = self.eval_expression(expression); + // return_value = possible_values; + } + } + } + // return_value + } +} + +#[must_use] +/// Run statements and return issues +pub fn run_processed( + statements: &Statements, + processed: &Processed, + database: &Arc, + check_child_scripts: bool, +) -> Vec { + let mut ignored_vars = Vec::new(); + ignored_vars.push("_this".to_string()); + let Ok(re1) = Regex::new(r"\/\/ ?IGNORE_PRIVATE_WARNING ?\[(.*)\]") else { + return Vec::new(); + }; + let Ok(re2) = Regex::new(r#""(.*?)""#) else { + return Vec::new(); + }; + for (_path, raw_source) in processed.sources() { + for (_, [ignores]) in re1.captures_iter(&raw_source).map(|c| c.extract()) { + for (_, [var]) in re2.captures_iter(ignores).map(|c| c.extract()) { + ignored_vars.push(var.to_ascii_lowercase()); + } + } + } + let mut scope = SciptScope::create(&HashSet::from_iter(ignored_vars), database, false); + scope.eval_statements(statements); + scope.finish(check_child_scripts).into_iter().collect() +} diff --git a/libs/sqf/src/analyze/lints/s12_inspector.rs b/libs/sqf/src/analyze/lints/s12_inspector.rs new file mode 100644 index 000000000..376ca1ae6 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s12_inspector.rs @@ -0,0 +1,252 @@ +use crate::{ + analyze::{ + inspector::{self, Issue}, + SqfLintData, + }, + 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}; +use tracing::trace; + +crate::lint!(LintS12Inspector); + +impl Lint for LintS12Inspector { + fn ident(&self) -> &str { + "inspector" + } + + fn sort(&self) -> u32 { + 120 + } + + fn description(&self) -> &str { + "Checks for code usage" + } + + fn documentation(&self) -> &str { +r"### Configuration +- **check_invalid_args**: [default: true] check_invalid_args (e.g. `x setFuel true`) +- **check_child_scripts**: [default: false] Checks oprhaned scripts. + Assumes un-called code will run in another scope (can cause false positives) + e.g. `private _var = 5; [{_var}] call CBA_fnc_addPerFrameEventHandler;` +- **check_undefined**: [default: true] Checks local vars that are not defined +- **check_not_private**: [default: true] Checks local vars that are not `private` +- **check_unused**: [default: false] Checks local vars that are never used +- **check_shadow**: [default: false] Checks local vars that are shaddowed + +```toml +[lints.sqf.inspector] +options.check_invalid_args = true +options.check_child_scripts = true +options.check_undefined = true +options.check_not_private = true +options.check_unused = true +options.check_shadow = true +```" + } + + fn default_config(&self) -> LintConfig { + LintConfig::help() + } + + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner for Runner { + type Target = Statements; + #[allow(clippy::too_many_lines)] + fn run( + &self, + _project: Option<&hemtt_common::config::ProjectConfig>, + config: &hemtt_common::config::LintConfig, + processed: Option<&hemtt_workspace::reporting::Processed>, + target: &Statements, + data: &SqfLintData, + ) -> hemtt_workspace::reporting::Codes { + let Some(processed) = processed else { + return Vec::new(); + }; + if !target.top_level { + // we only want to handle full files, not all the sub-statements + return Vec::new(); + }; + let (_addon, database) = data; + + let check_invalid_args = + if let Some(toml::Value::Boolean(b)) = config.option("check_invalid_args") { + *b + } else { + true + }; + let check_child_scripts = + if let Some(toml::Value::Boolean(b)) = config.option("check_child_scripts") { + *b + } else { + false // can cause false positives + }; + let check_undefined = + if let Some(toml::Value::Boolean(b)) = config.option("check_undefined") { + *b + } else { + true + }; + let check_not_private = if let Some(toml::Value::Boolean(b)) = + config.option("check_not_private") + { + *b + } else { + true + }; + let check_unused = + if let Some(toml::Value::Boolean(b)) = config.option("check_unused") { + *b + } else { + false + }; + let check_shadow = + if let Some(toml::Value::Boolean(b)) = config.option("check_shadow") { + *b + } else { + false + }; + + let mut errors: Codes = Vec::new(); + let issues = inspector::run_processed(target, processed, database, check_child_scripts); + trace!("issues {}", issues.len()); + + for issue in issues { + match issue { + Issue::InvalidArgs(cmd, range) => { + if check_invalid_args { + errors.push(Arc::new(CodeS12Inspector::new( + range, + format!("Bad Args: {cmd}"), + None, + config.severity(), + processed, + ))); + } + } + Issue::Undefined(var, range, is_child) => { + if check_undefined { + let error_hint = if is_child {Some("From Child Code - may not be a problem".to_owned())} else {None}; + errors.push(Arc::new(CodeS12Inspector::new( + range, + format!("Undefined: {var}"), + error_hint, + config.severity(), + processed, + ))); + } + } + Issue::NotPrivate(var, range) => { + if check_not_private { + errors.push(Arc::new(CodeS12Inspector::new( + range, + format!("NotPrivate: {var}"), + None, + config.severity(), + processed, + ))); + } + } + Issue::Unused(var, range) => { + if check_unused { + errors.push(Arc::new(CodeS12Inspector::new( + range, + format!("Unused: {var}"), + None, + config.severity(), + processed, + ))); + } + } + Issue::Shadowed(var, range) => { + if check_shadow { + errors.push(Arc::new(CodeS12Inspector::new( + range, + format!("Shadowed: {var}"), + None, + config.severity(), + processed, + ))); + } + } + }; + } + + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS12Inspector { + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS12Inspector { + fn ident(&self) -> &'static str { + "L-S12" + } + + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#inspector") + } + /// Top message + fn message(&self) -> String { + format!("Inspector - {}", self.error_type) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + + fn severity(&self) -> Severity { + self.severity + } + + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS12Inspector { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/analyze/mod.rs b/libs/sqf/src/analyze/mod.rs index 7c7ae3b4a..4a310a875 100644 --- a/libs/sqf/src/analyze/mod.rs +++ b/libs/sqf/src/analyze/mod.rs @@ -2,6 +2,8 @@ pub mod lints { automod::dir!(pub "src/analyze/lints"); } +pub mod inspector; + use std::sync::Arc; use hemtt_common::config::ProjectConfig; diff --git a/libs/sqf/src/lib.rs b/libs/sqf/src/lib.rs index fb2406017..3174a6ba3 100644 --- a/libs/sqf/src/lib.rs +++ b/libs/sqf/src/lib.rs @@ -23,6 +23,7 @@ pub struct Statements { /// This isn't required to actually be anything significant, but will be displayed in-game if a script error occurs. source: Arc, span: Range, + top_level: bool, } impl Statements { diff --git a/libs/sqf/src/parser/mod.rs b/libs/sqf/src/parser/mod.rs index 108b4a6e4..6634352b1 100644 --- a/libs/sqf/src/parser/mod.rs +++ b/libs/sqf/src/parser/mod.rs @@ -43,6 +43,7 @@ pub fn run(database: &Database, processed: &Processed) -> Result( source: processed.extract(span.clone()), span, content, + top_level: false, }) }) } diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs new file mode 100644 index 000000000..7c2e065f3 --- /dev/null +++ b/libs/sqf/tests/inspector.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use hemtt_sqf::Statements; +use hemtt_workspace::reporting::Processed; + +pub use float_ord::FloatOrd as Scalar; +use hemtt_preprocessor::Processor; +use hemtt_sqf::parser::database::Database; +use hemtt_workspace::LayerType; +const ROOT: &str = "tests/inspector/"; + +fn get_statements(file: &str) -> (Processed, Statements, Arc) { + let folder = std::path::PathBuf::from(ROOT); + let workspace = hemtt_workspace::Workspace::builder() + .physical(&folder, LayerType::Source) + .finish(None, false, &hemtt_common::config::PDriveOption::Disallow) + .unwrap(); + let source = workspace.join(file).unwrap(); + let processed = Processor::run(&source).unwrap(); + let statements = hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap(); + let database = Arc::new(Database::a3(false)); + (processed, statements, database) +} + +#[cfg(test)] +mod tests { + use crate::get_statements; + use hemtt_sqf::analyze::inspector::{self, Issue}; + + #[test] + pub fn test_0() { + let (pro, sqf, database) = get_statements("test_0.sqf"); + let result = inspector::run_processed(&sqf, &pro, &database, true); + println!("done: {}, {result:?}", result.len()); + } + + #[test] + pub fn test_1() { + let (pro, sqf, database) = get_statements("test_1.sqf"); + let result = inspector::run_processed(&sqf, &pro, &database, true); + println!("done: {result:?}"); + assert_eq!(result.len(), 6); + // Order not guarenteed + assert!(result + .iter() + .find(|i| { + if let Issue::InvalidArgs(cmd, _) = i { + cmd == "[B:setFuel]" + } else { + false + } + }) + .is_some()); + assert!(result + .iter() + .find(|i| { + if let Issue::Undefined(var, _, _) = i { + var == "_guy" + } else { + false + } + }) + .is_some()); + assert!(result + .iter() + .find(|i| { + if let Issue::NotPrivate(var, _) = i { + var == "_z" + } else { + false + } + }) + .is_some()); + assert!(result + .iter() + .find(|i| { + if let Issue::Unused(var, _) = i { + var == "_c" + } else { + false + } + }) + .is_some()); + assert!(result + .iter() + .find(|i| { + if let Issue::Shadowed(var, _) = i { + var == "_var1" + } else { + false + } + }) + .is_some()); + assert!(result + .iter() + .find(|i| { + if let Issue::InvalidArgs(var, _) = i { + var == "[B:addPublicVariableEventHandler]" + } else { + false + } + }) + .is_some()); + } +} diff --git a/libs/sqf/tests/inspector/test_0.sqf b/libs/sqf/tests/inspector/test_0.sqf new file mode 100644 index 000000000..e69de29bb diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf new file mode 100644 index 000000000..8c020b5f3 --- /dev/null +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -0,0 +1,48 @@ +x setFuel true; // args: takes number 0-1 +x setFuel f; +_guy setDamage 1; // _guy undefeind +private _you = player; +_you setDamage 0.5; +_z = 7; // not private +systemChat str _z; +private "_a"; +_a = 8; +private ["_b"]; +_b = _a + 1; +private _c = _b; // unused +params ["_var1"]; +private _var1 = 10; // shadow +diag_log text str _var1; +gx = []; +gx addPublicVariableEventHandler {}; // args: takes lhs string + +for "_i" from 1 to 20 step 0.5 do { + systemChat str _i; +}; +for [{private _k = 0}, {_k < 5}, {_k = _k + 1}] do { + systemChat str _k; +}; + +//IGNORE_PRIVATE_WARNING["_fromUpper"]; +X = _fromUpper; + +[] call { + private "_weird"; + //IGNORE_PRIVATE_WARNING["_weird"] - // No way to know the order is different + for "_i" from 1 to 5 do { + if (_i%2 == 0) then { + truck lock _weird; + }; + if (_i%2 == 1) then { + _weird = 0.5; + }; + }; +}; + +// IGNORE_PRIVATE_WARNING["somePFEH"] - // otherwise will assume it's nil +if (z) then { + somePFEH = nil; +}; +if (y) then { + setObjectViewDistance somePFEH; +}; diff --git a/libs/sqf/tests/lints.rs b/libs/sqf/tests/lints.rs index 50331c525..2547454ab 100644 --- a/libs/sqf/tests/lints.rs +++ b/libs/sqf/tests/lints.rs @@ -81,3 +81,4 @@ analyze!(s07_select_parse_number); analyze!(s08_format_args); analyze!(s09_banned_command); analyze!(s11_if_not_else); +analyze!(s12_inspector); diff --git a/libs/sqf/tests/lints/project_tests.toml b/libs/sqf/tests/lints/project_tests.toml index bf02cf4af..e18d39f17 100644 --- a/libs/sqf/tests/lints/project_tests.toml +++ b/libs/sqf/tests/lints/project_tests.toml @@ -6,5 +6,8 @@ options.ignore = [ "addPublicVariableEventHandler", ] +[lints.sqf.inspector] +options.check_shadow = true + [lints.sqf] if_not_else = true diff --git a/libs/sqf/tests/lints/s06_find_in_str/source.sqf b/libs/sqf/tests/lints/s06_find_in_str/source.sqf index d35ed0886..97e87e205 100644 --- a/libs/sqf/tests/lints/s06_find_in_str/source.sqf +++ b/libs/sqf/tests/lints/s06_find_in_str/source.sqf @@ -1,2 +1,2 @@ "foobar" find "bar" > -1; -private _hasBar = _things find "bar" > -1; +private _hasBar = things find "bar" > -1; diff --git a/libs/sqf/tests/lints/s06_find_in_str/stdout.ansi b/libs/sqf/tests/lints/s06_find_in_str/stdout.ansi index a9457ac6c..bd696b263 100644 --- a/libs/sqf/tests/lints/s06_find_in_str/stdout.ansi +++ b/libs/sqf/tests/lints/s06_find_in_str/stdout.ansi @@ -10,8 +10,8 @@ help[L-S06]: string search using `in` is faster than `find` ┌─ source.sqf:2:19 │ -2 │ private _hasBar = _things find "bar" > -1; - │ ^^^^^^^^^^^^^^^^^^^^^^^ using `find` with -1 +2 │ private _hasBar = things find "bar" > -1; + │ ^^^^^^^^^^^^^^^^^^^^^^ using `find` with -1 │ - = try: "bar" in _things + = try: "bar" in things diff --git a/libs/sqf/tests/lints/s12_inspector/source.sqf b/libs/sqf/tests/lints/s12_inspector/source.sqf new file mode 100644 index 000000000..2c423c03b --- /dev/null +++ b/libs/sqf/tests/lints/s12_inspector/source.sqf @@ -0,0 +1,4 @@ +x setFuel true; // args: takes number 0-1 + +params ["_var1"]; +private _var1 = 7; \ No newline at end of file diff --git a/libs/sqf/tests/lints/s12_inspector/stdout.ansi b/libs/sqf/tests/lints/s12_inspector/stdout.ansi new file mode 100644 index 000000000..5eb6ccd6f --- /dev/null +++ b/libs/sqf/tests/lints/s12_inspector/stdout.ansi @@ -0,0 +1,13 @@ +help[L-S12]: Inspector - Bad Args: [B:setFuel] + ┌─ source.sqf:1:3 + │ +1 │ x setFuel true; // args: takes number 0-1 + │ ^^^^^^^ + + +help[L-S12]: Inspector - Shadowed: _var1 + ┌─ source.sqf:4:1 + │ +4 │ private _var1 = 7; + │ ^^^^^^^^^^^^^^^^^ + From c8ad10eccd8ad5d5e71c5881b6e41f8ec1f059ca Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Thu, 26 Sep 2024 22:49:52 -0500 Subject: [PATCH 02/24] Handle common external funcs, only show shadowing at same level --- libs/sqf/src/analyze/inspector/commands.rs | 312 +++++++++++++++++ .../analyze/inspector/external_functions.rs | 108 ++++++ libs/sqf/src/analyze/inspector/mod.rs | 322 +----------------- libs/sqf/tests/inspector.rs | 119 ++++--- libs/sqf/tests/inspector/test_1.sqf | 30 ++ libs/sqf/tests/lints/s12_inspector/source.sqf | 2 +- .../sqf/tests/lints/s12_inspector/stdout.ansi | 14 +- 7 files changed, 528 insertions(+), 379 deletions(-) create mode 100644 libs/sqf/src/analyze/inspector/commands.rs create mode 100644 libs/sqf/src/analyze/inspector/external_functions.rs diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs new file mode 100644 index 000000000..8be99afe1 --- /dev/null +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -0,0 +1,312 @@ +use std::{collections::HashSet, ops::Range}; + +use crate::{analyze::inspector::VarSource, Expression}; +use tracing::error; + +use super::{game_value::GameValue, SciptScope}; + +impl SciptScope { + #[must_use] + pub fn cmd_u_private(&mut self, rhs: &HashSet) -> HashSet { + fn push_var(s: &mut SciptScope, var: &String, source: &Range) { + if s.ignored_vars.contains(&var.to_ascii_lowercase()) { + s.var_assign( + &var.to_string(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Ignore, + ); + } else { + s.var_assign( + &var.to_string(), + true, + HashSet::from([GameValue::Nothing]), + VarSource::Real(source.clone()), + ); + } + } + for possible in rhs { + if let GameValue::Array(Some(Expression::Array(array, _))) = possible { + for element in array { + let Expression::String(var, source, _) = element else { + continue; + }; + if var.is_empty() { + continue; + } + push_var(self, &var.to_string(), source); + } + } + if let GameValue::String(Some(Expression::String(var, source, _))) = possible { + if var.is_empty() { + continue; + } + push_var(self, &var.to_string(), source); + } + } + HashSet::new() + } + #[must_use] + pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + continue; + }; + + for entry in array { + match entry { + Expression::String(var, source, _) => { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Real(source.clone()), + ); + } + Expression::Array(var_array, _) => { + if !var_array.is_empty() { + if let Expression::String(var, source, _) = &var_array[0] { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Real(source.clone()), + ); + } + } + } + _ => {} + } + } + } + HashSet::from([GameValue::Boolean(None)]) + } + #[must_use] + pub fn cmd_generic_call(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + self.code_used.insert(expression.clone()); + self.eval_statements(statements); + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_b_do( + &mut self, + lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + // look for forType vars with valid strings (ignore old style code) + let mut do_run = true; + for possible in lhs { + if let GameValue::ForType(option) = possible { + match option { + Some(Expression::String(var, source, _)) => { + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Number(None)]), + VarSource::Real(source.clone()), + ); + } + Some(Expression::Array(array, _)) => { + if array.len() != 3 { + error!("for wrong len"); + continue; + } + for for_stage in array { + let Expression::Code(for_statements) = for_stage else { + continue; + }; + self.code_used.insert(for_stage.clone()); + self.eval_statements(for_statements); + } + } + None => { + do_run = false; + } + _ => { + unreachable!(""); + } + } + } + } + self.code_used.insert(expression.clone()); + if do_run { + self.eval_statements(statements); + } + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_generic_call_magic( + &mut self, + code_possibilities: &HashSet, + magic: &Vec, + source: &Range, + ) -> HashSet { + for possible in code_possibilities { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + for var in magic { + self.var_assign( + var, + true, + HashSet::from([GameValue::Anything]), + VarSource::Magic(source.clone()), + ); + } + self.code_used.insert(expression.clone()); + self.eval_statements(statements); + self.pop(); + } + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_for(&mut self, rhs: &HashSet) -> HashSet { + let mut return_value = HashSet::new(); + for possible in rhs { + match possible { + GameValue::Array(option) | GameValue::String(option) => { + return_value.insert(GameValue::ForType(option.clone())); + } + _ => { + error!("shouldn't be reachable?"); + return_value.insert(GameValue::ForType(None)); + } + } + } + return_value + } + #[must_use] + /// for (from, to, step) chained commands + pub fn cmd_b_from_chain( + &self, + lhs: &HashSet, + _rhs: &HashSet, + ) -> HashSet { + lhs.clone() + } + #[must_use] + pub fn cmd_u_is_nil(&mut self, rhs: &HashSet) -> HashSet { + let mut non_string = false; + for possible in rhs { + let GameValue::String(possible) = possible else { + non_string = true; + continue; + }; + let Some(expression) = possible else { + continue; + }; + let Expression::String(var, _, _) = expression else { + continue; + }; + let _ = self.var_retrieve(var, &expression.span(), true); + } + if non_string { + let _ = self.cmd_generic_call(rhs); + } + HashSet::from([GameValue::Boolean(None)]) + } + #[must_use] + pub fn cmd_b_then( + &mut self, + _lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + let mut return_value = HashSet::new(); + for possible in rhs { + if let GameValue::Code(Some(Expression::Code(_statements))) = possible { + return_value.extend(self.cmd_generic_call(rhs)); + } + if let GameValue::Array(Some(Expression::Array(array, _))) = possible { + for expression in array { + return_value.extend(self.cmd_generic_call(&HashSet::from([GameValue::Code( + Some(expression.clone()), + )]))); + } + } + } + return_value + } + #[must_use] + pub fn cmd_b_else( + &self, + lhs: &HashSet, + rhs: &HashSet, + ) -> HashSet { + let mut return_value = HashSet::new(); // just merge, not really the same but should be fine + for possible in rhs { + return_value.insert(possible.clone()); + } + for possible in lhs { + return_value.insert(possible.clone()); + } + return_value + } + #[must_use] + pub fn cmd_b_get_or_default_call(&mut self, rhs: &HashSet) -> HashSet { + let mut possible_code = HashSet::new(); + for possible in rhs { + let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + continue; + }; + if array.len() < 2 { + continue; + } + possible_code.insert(GameValue::Code(Some(array[1].clone()))); + } + let _ = self.cmd_generic_call(&possible_code); + HashSet::from([GameValue::Anything]) + } + #[must_use] + pub fn cmd_u_to_string(&mut self, rhs: &HashSet) -> HashSet { + for possible in rhs { + let GameValue::Code(Some(expression)) = possible else { + continue; + }; + let Expression::Code(_) = expression else { + continue; + }; + // just skip because it will often use a _x + self.code_used.insert(expression.clone()); + } + HashSet::from([GameValue::String(None)]) + } +} diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs new file mode 100644 index 000000000..311709fa1 --- /dev/null +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -0,0 +1,108 @@ +use std::collections::HashSet; + +use crate::{analyze::inspector::VarSource, Expression}; + +use super::{game_value::GameValue, SciptScope}; + +impl SciptScope { + pub fn external_function(&mut self, lhs: &HashSet, rhs: &Expression) { + let Expression::Variable(var, _) = rhs else { + return; + }; + for possible in lhs { + let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + continue; + }; + match var.to_ascii_lowercase().as_str() { + "cba_fnc_hasheachpair" | "cba_fnc_hashfilter" => { + if array.len() > 1 { + let code = self.eval_expression(&array[1]); + self.external_current_scope( + &code, + &vec![ + ("_key", GameValue::Anything), + ("_value", GameValue::Anything), + ], + ); + } + } + "cba_fnc_filter" => { + if array.len() > 1 { + let code = self.eval_expression(&array[1]); + self.external_current_scope(&code, &vec![("_x", GameValue::Anything)]); + } + } + "cba_fnc_directcall" => { + if !array.is_empty() { + let code = self.eval_expression(&array[0]); + self.external_current_scope(&code, &vec![]); + } + } + "ace_interact_menu_fnc_createaction" => { + for index in 3..5 { + if array.len() > index { + let code = self.eval_expression(&array[index]); + self.external_new_scope( + &code, + &vec![ + ("_target", GameValue::Object), + ("_player", GameValue::Object), + ], + ); + } + } + } + _ => {} + } + } + } + fn external_new_scope( + &mut self, + possible_arg: &HashSet, + vars: &Vec<(&str, GameValue)>, + ) { + for element in possible_arg { + let GameValue::Code(Some(expression)) = element else { + continue; + }; + let Expression::Code(statements) = expression else { + return; + }; + if self.code_used.contains(expression) { + return; + } + let mut ext_scope = Self::create(&self.ignored_vars, &self.database, true); + + for (var, value) in vars { + ext_scope.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); + } + self.code_used.insert(expression.clone()); + ext_scope.eval_statements(statements); + self.errors.extend(ext_scope.finish(false)); + } + } + fn external_current_scope( + &mut self, + possible_arg: &HashSet, + vars: &Vec<(&str, GameValue)>, + ) { + for element in possible_arg { + let GameValue::Code(Some(expression)) = element else { + continue; + }; + let Expression::Code(statements) = expression else { + continue; + }; + if self.code_used.contains(expression) { + continue; + } + self.push(); + for (var, value) in vars { + self.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); + } + self.code_used.insert(expression.clone()); + self.eval_statements(statements); + self.pop(); + } + } +} diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 0ea2c936a..e27ca3d55 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -16,6 +16,8 @@ use hemtt_workspace::reporting::Processed; use regex::Regex; use tracing::{error, trace}; +mod commands; +mod external_functions; mod game_value; #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -169,7 +171,8 @@ impl SciptScope { )); } } else if local { - if source.check_shadow() { + // Only check shadowing inside the same scope-level (could make an option) + if source.check_shadow() && stack_level_search.unwrap_or_default() == 0 { self.errors.insert(Issue::Shadowed( var.to_owned(), source.get_range().unwrap_or_default(), @@ -237,309 +240,6 @@ impl SciptScope { set } } - #[must_use] - pub fn cmd_u_private(&mut self, rhs: &HashSet) -> HashSet { - fn push_var(s: &mut SciptScope, var: &String, source: &Range) { - if s.ignored_vars.contains(&var.to_ascii_lowercase()) { - s.var_assign( - &var.to_string(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Ignore, - ); - } else { - s.var_assign( - &var.to_string(), - true, - HashSet::from([GameValue::Nothing]), - VarSource::Real(source.clone()), - ); - } - } - for possible in rhs { - if let GameValue::Array(Some(Expression::Array(array, _))) = possible { - for element in array { - let Expression::String(var, source, _) = element else { - continue; - }; - if var.is_empty() { - continue; - } - push_var(self, &var.to_string(), source); - } - } - if let GameValue::String(Some(Expression::String(var, source, _))) = possible { - if var.is_empty() { - continue; - } - push_var(self, &var.to_string(), source); - } - } - HashSet::new() - } - #[must_use] - pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { - for possible in rhs { - let GameValue::Array(Some(Expression::Array(array, _))) = possible else { - continue; - }; - - for entry in array { - match entry { - Expression::String(var, source, _) => { - if var.is_empty() { - continue; - } - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Real(source.clone()), - ); - } - Expression::Array(var_array, _) => { - if !var_array.is_empty() { - if let Expression::String(var, source, _) = &var_array[0] { - if var.is_empty() { - continue; - } - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Real(source.clone()), - ); - } - } - } - _ => {} - } - } - } - HashSet::from([GameValue::Boolean(None)]) - } - #[must_use] - pub fn cmd_generic_call(&mut self, rhs: &HashSet) -> HashSet { - for possible in rhs { - let GameValue::Code(Some(expression)) = possible else { - continue; - }; - let Expression::Code(statements) = expression else { - continue; - }; - if self.code_used.contains(expression) { - continue; - } - self.push(); - self.code_used.insert(expression.clone()); - self.eval_statements(statements); - self.pop(); - } - HashSet::from([GameValue::Anything]) - } - #[must_use] - pub fn cmd_b_do( - &mut self, - lhs: &HashSet, - rhs: &HashSet, - ) -> HashSet { - for possible in rhs { - let GameValue::Code(Some(expression)) = possible else { - continue; - }; - let Expression::Code(statements) = expression else { - continue; - }; - if self.code_used.contains(expression) { - continue; - } - self.push(); - // look for forType vars with valid strings (ignore old style code) - let mut do_run = true; - for possible in lhs { - if let GameValue::ForType(option) = possible { - match option { - Some(Expression::String(var, source, _)) => { - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Number(None)]), - VarSource::Real(source.clone()), - ); - } - Some(Expression::Array(array, _)) => { - if array.len() != 3 { - error!("for wrong len"); - continue; - } - for for_stage in array { - let Expression::Code(for_statements) = for_stage else { - continue; - }; - self.code_used.insert(for_stage.clone()); - self.eval_statements(for_statements); - } - } - None => { - do_run = false; - } - _ => { - unreachable!(""); - } - } - } - } - self.code_used.insert(expression.clone()); - if do_run { - self.eval_statements(statements); - } - self.pop(); - } - HashSet::from([GameValue::Anything]) - } - #[must_use] - pub fn cmd_generic_call_magic( - &mut self, - code_possibilities: &HashSet, - magic: &Vec, - source: &Range, - ) -> HashSet { - for possible in code_possibilities { - let GameValue::Code(Some(expression)) = possible else { - continue; - }; - let Expression::Code(statements) = expression else { - continue; - }; - if self.code_used.contains(expression) { - continue; - } - self.push(); - for var in magic { - self.var_assign( - var, - true, - HashSet::from([GameValue::Anything]), - VarSource::Magic(source.clone()), - ); - } - self.code_used.insert(expression.clone()); - self.eval_statements(statements); - self.pop(); - } - HashSet::from([GameValue::Anything]) - } - #[must_use] - pub fn cmd_for(&mut self, rhs: &HashSet) -> HashSet { - let mut return_value = HashSet::new(); - for possible in rhs { - match possible { - GameValue::Array(option) | GameValue::String(option) => { - return_value.insert(GameValue::ForType(option.clone())); - } - _ => { - error!("shouldn't be reachable?"); - return_value.insert(GameValue::ForType(None)); - } - } - } - return_value - } - #[must_use] - /// for (from, to, step) chained commands - pub fn cmd_b_from_chain( - &self, - lhs: &HashSet, - _rhs: &HashSet, - ) -> HashSet { - lhs.clone() - } - #[must_use] - pub fn cmd_u_is_nil(&mut self, rhs: &HashSet) -> HashSet { - let mut non_string = false; - for possible in rhs { - let GameValue::String(possible) = possible else { - non_string = true; - continue; - }; - let Some(expression) = possible else { - continue; - }; - let Expression::String(var, _, _) = expression else { - continue; - }; - let _ = self.var_retrieve(var, &expression.span(), true); - } - if non_string { - let _ = self.cmd_generic_call(rhs); - } - HashSet::from([GameValue::Boolean(None)]) - } - #[must_use] - pub fn cmd_b_then( - &mut self, - _lhs: &HashSet, - rhs: &HashSet, - ) -> HashSet { - let mut return_value = HashSet::new(); - for possible in rhs { - if let GameValue::Code(Some(Expression::Code(_statements))) = possible { - return_value.extend(self.cmd_generic_call(rhs)); - } - if let GameValue::Array(Some(Expression::Array(array, _))) = possible { - for expression in array { - return_value.extend(self.cmd_generic_call(&HashSet::from([GameValue::Code( - Some(expression.clone()), - )]))); - } - } - } - return_value - } - #[must_use] - pub fn cmd_b_else( - &self, - lhs: &HashSet, - rhs: &HashSet, - ) -> HashSet { - let mut return_value = HashSet::new(); // just merge, not really the same but should be fine - for possible in rhs { - return_value.insert(possible.clone()); - } - for possible in lhs { - return_value.insert(possible.clone()); - } - return_value - } - #[must_use] - pub fn cmd_b_get_or_default_call(&mut self, rhs: &HashSet) -> HashSet { - let mut possible_code = HashSet::new(); - for possible in rhs { - let GameValue::Array(Some(Expression::Array(array, _))) = possible else { - continue; - }; - if array.len() < 2 { - continue; - } - possible_code.insert(GameValue::Code(Some(array[1].clone()))); - } - let _ = self.cmd_generic_call(&possible_code); - HashSet::from([GameValue::Anything]) - } - #[must_use] - pub fn cmd_u_to_string(&mut self, rhs: &HashSet) -> HashSet { - for possible in rhs { - let GameValue::Code(Some(expression)) = possible else { - continue; - }; - let Expression::Code(_) = expression else { - continue; - }; - // just skip because it will often use a _x - self.code_used.insert(expression.clone()); - } - HashSet::from([GameValue::String(None)]) - } #[must_use] #[allow(clippy::too_many_lines)] @@ -620,7 +320,10 @@ impl SciptScope { BinaryCommand::Else => Some(self.cmd_b_else(&lhs_set, &rhs_set)), BinaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { "params" => Some(self.cmd_generic_params(&rhs_set)), - "call" => Some(self.cmd_generic_call(&rhs_set)), + "call" => { + self.external_function(&lhs_set, rhs); + Some(self.cmd_generic_call(&rhs_set)) + } "exitwith" => { // todo: handle scope exits Some(self.cmd_generic_call(&rhs_set)) @@ -718,8 +421,8 @@ pub fn run_processed( database: &Arc, check_child_scripts: bool, ) -> Vec { - let mut ignored_vars = Vec::new(); - ignored_vars.push("_this".to_string()); + let mut ignored_vars = HashSet::new(); + ignored_vars.insert("_this".to_string()); let Ok(re1) = Regex::new(r"\/\/ ?IGNORE_PRIVATE_WARNING ?\[(.*)\]") else { return Vec::new(); }; @@ -729,11 +432,12 @@ pub fn run_processed( for (_path, raw_source) in processed.sources() { for (_, [ignores]) in re1.captures_iter(&raw_source).map(|c| c.extract()) { for (_, [var]) in re2.captures_iter(ignores).map(|c| c.extract()) { - ignored_vars.push(var.to_ascii_lowercase()); + ignored_vars.insert(var.to_ascii_lowercase()); } } } - let mut scope = SciptScope::create(&HashSet::from_iter(ignored_vars), database, false); + + let mut scope = SciptScope::create(&ignored_vars, database, false); scope.eval_statements(statements); scope.finish(check_child_scripts).into_iter().collect() } diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs index 7c2e065f3..f6b0f74f7 100644 --- a/libs/sqf/tests/inspector.rs +++ b/libs/sqf/tests/inspector.rs @@ -38,68 +38,63 @@ mod tests { pub fn test_1() { let (pro, sqf, database) = get_statements("test_1.sqf"); let result = inspector::run_processed(&sqf, &pro, &database, true); - println!("done: {result:?}"); - assert_eq!(result.len(), 6); + assert_eq!(result.len(), 8); // Order not guarenteed - assert!(result - .iter() - .find(|i| { - if let Issue::InvalidArgs(cmd, _) = i { - cmd == "[B:setFuel]" - } else { - false - } - }) - .is_some()); - assert!(result - .iter() - .find(|i| { - if let Issue::Undefined(var, _, _) = i { - var == "_guy" - } else { - false - } - }) - .is_some()); - assert!(result - .iter() - .find(|i| { - if let Issue::NotPrivate(var, _) = i { - var == "_z" - } else { - false - } - }) - .is_some()); - assert!(result - .iter() - .find(|i| { - if let Issue::Unused(var, _) = i { - var == "_c" - } else { - false - } - }) - .is_some()); - assert!(result - .iter() - .find(|i| { - if let Issue::Shadowed(var, _) = i { - var == "_var1" - } else { - false - } - }) - .is_some()); - assert!(result - .iter() - .find(|i| { - if let Issue::InvalidArgs(var, _) = i { - var == "[B:addPublicVariableEventHandler]" - } else { - false - } - }) - .is_some()); + assert!(result.iter().any(|i| { + if let Issue::InvalidArgs(cmd, _) = i { + cmd == "[B:setFuel]" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Undefined(var, _, _) = i { + var == "_guy" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::NotPrivate(var, _) = i { + var == "_z" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Unused(var, _) = i { + var == "_c" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Shadowed(var, _) = i { + var == "_var1" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::InvalidArgs(var, _) = i { + var == "[B:addPublicVariableEventHandler]" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::InvalidArgs(var, _) = i { + var == "[B:ctrlSetText]" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Undefined(var, _, _) = i { + var == "_myLocalVar1" + } else { + false + } + })); } } diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index 8c020b5f3..b39b4ff27 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -46,3 +46,33 @@ if (z) then { if (y) then { setObjectViewDistance somePFEH; }; + +private _condition = + { + [_player, _target] call y + }; +[ + "", + localize "STR_A3_Arsenal", + "", + { + x ctrlSetText _player; // bad arg type + [_target, _player] call z; + }, + _condition +] call ace_interact_menu_fnc_createAction; + +private _hash = [] call CBA_fnc_hashCreate; +private _dumpHash = { + diag_log format ["Key: %1, Value: %2", _key, _value]; +}; +[_hash, _dumpHash] call CBA_fnc_hashEachPair; + +private _myLocalVar1 = 555; +_myLocalVar1 = _myLocalVar1 + 1; +[{ + systemChat str _myLocalVar1; // invalid +}, 0, []] call CBA_fnc_addPerFrameHandler; + +private _myLocalVar2 = 55; +[{systemChat str _myLocalVar2}] call CBA_fnc_directCall; // fine diff --git a/libs/sqf/tests/lints/s12_inspector/source.sqf b/libs/sqf/tests/lints/s12_inspector/source.sqf index 2c423c03b..e91864ff9 100644 --- a/libs/sqf/tests/lints/s12_inspector/source.sqf +++ b/libs/sqf/tests/lints/s12_inspector/source.sqf @@ -1,4 +1,4 @@ x setFuel true; // args: takes number 0-1 params ["_var1"]; -private _var1 = 7; \ No newline at end of file +private _var1 = 7; diff --git a/libs/sqf/tests/lints/s12_inspector/stdout.ansi b/libs/sqf/tests/lints/s12_inspector/stdout.ansi index 5eb6ccd6f..736c60c9b 100644 --- a/libs/sqf/tests/lints/s12_inspector/stdout.ansi +++ b/libs/sqf/tests/lints/s12_inspector/stdout.ansi @@ -1,13 +1,13 @@ -help[L-S12]: Inspector - Bad Args: [B:setFuel] - ┌─ source.sqf:1:3 - │ -1 │ x setFuel true; // args: takes number 0-1 - │ ^^^^^^^ - - help[L-S12]: Inspector - Shadowed: _var1 ┌─ source.sqf:4:1 │ 4 │ private _var1 = 7; │ ^^^^^^^^^^^^^^^^^ + +help[L-S12]: Inspector - Bad Args: [B:setFuel] + ┌─ source.sqf:1:3 + │ +1 │ x setFuel true; // args: takes number 0-1 + │ ^^^^^^^ + From 03f3ac935b0c6fd2af944e39e3c62a079f0ca35f Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sat, 28 Sep 2024 00:27:35 -0500 Subject: [PATCH 03/24] Check arg arrays --- Cargo.lock | 3 +- Cargo.toml | 3 +- libs/sqf/src/analyze/inspector/commands.rs | 179 ++++++++++-------- .../analyze/inspector/external_functions.rs | 60 +++--- libs/sqf/src/analyze/inspector/game_value.rs | 103 ++++++++-- libs/sqf/src/analyze/inspector/mod.rs | 50 +++-- libs/sqf/src/analyze/lints/s12_inspector.rs | 11 +- libs/sqf/tests/inspector.rs | 52 +++-- libs/sqf/tests/inspector/test_1.sqf | 76 +++++--- libs/sqf/tests/inspector/test_2.sqf | 26 +++ 10 files changed, 373 insertions(+), 190 deletions(-) create mode 100644 libs/sqf/tests/inspector/test_2.sqf diff --git a/Cargo.lock b/Cargo.lock index c21014d2f..dc123f084 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,8 +190,7 @@ dependencies = [ [[package]] name = "arma3-wiki" version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "845a808d6724494856cec8655fd9f64c73d3e52cdaf3f3d2eb3a247af8f29149" +source = "git+https://github.com/acemod/arma3-wiki.git?rev=cd147399fecd21253cba1e5348b7f0e7d7837948#cd147399fecd21253cba1e5348b7f0e7d7837948" dependencies = [ "directories", "fs_extra", diff --git a/Cargo.toml b/Cargo.toml index a10e3dc95..6012579bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,8 @@ future_incompatible = "warn" nonstandard_style = "warn" [workspace.dependencies] -arma3-wiki = "0.3.2" +#arma3-wiki = "0.3.2" +arma3-wiki = { git = "https://github.com/acemod/arma3-wiki.git", rev = "cd147399fecd21253cba1e5348b7f0e7d7837948" } automod = "1.0.14" byteorder = "1.5.0" chumsky = "0.9.3" diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index 8be99afe1..da6911be4 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -1,7 +1,6 @@ use std::{collections::HashSet, ops::Range}; use crate::{analyze::inspector::VarSource, Expression}; -use tracing::error; use super::{game_value::GameValue, SciptScope}; @@ -21,20 +20,23 @@ impl SciptScope { &var.to_string(), true, HashSet::from([GameValue::Nothing]), - VarSource::Real(source.clone()), + VarSource::Private(source.clone()), ); } } for possible in rhs { - if let GameValue::Array(Some(Expression::Array(array, _))) = possible { - for element in array { - let Expression::String(var, source, _) = element else { - continue; - }; - if var.is_empty() { - continue; + if let GameValue::Array(Some(gv_array)) = possible { + for gv_index in gv_array { + for element in gv_index { + let GameValue::String(Some(Expression::String(var, source, _))) = element + else { + continue; + }; + if var.is_empty() { + continue; + } + push_var(self, &var.to_string(), source); } - push_var(self, &var.to_string(), source); } } if let GameValue::String(Some(Expression::String(var, source, _))) = possible { @@ -49,39 +51,46 @@ impl SciptScope { #[must_use] pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { for possible in rhs { - let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + let GameValue::Array(Some(gv_array)) = possible else { continue; }; - for entry in array { - match entry { - Expression::String(var, source, _) => { - if var.is_empty() { - continue; + for gv_index in gv_array { + for element in gv_index { + match element { + GameValue::String(Some(Expression::String(var, source, _))) => { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Params(source.clone()), + ); } - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Real(source.clone()), - ); - } - Expression::Array(var_array, _) => { - if !var_array.is_empty() { - if let Expression::String(var, source, _) = &var_array[0] { - if var.is_empty() { - continue; + GameValue::Array(Some(gv_array)) => { + if gv_array.is_empty() { + continue; + } + for element in &gv_array[0] { + if let GameValue::String(Some(Expression::String(var, source, _))) = + element + { + if var.is_empty() { + continue; + } + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Anything]), + VarSource::Params(source.clone()), + ); } - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Real(source.clone()), - ); } } + _ => {} } - _ => {} } } } @@ -123,39 +132,32 @@ impl SciptScope { continue; } self.push(); - // look for forType vars with valid strings (ignore old style code) - let mut do_run = true; + let mut do_run = false; for possible in lhs { if let GameValue::ForType(option) = possible { - match option { - Some(Expression::String(var, source, _)) => { - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Number(None)]), - VarSource::Real(source.clone()), - ); - } - Some(Expression::Array(array, _)) => { - if array.len() != 3 { - error!("for wrong len"); - continue; + let Some(for_args_array) = option else { + continue; + }; + do_run = true; + for stage in for_args_array { + match stage { + Expression::String(var, source, _) => { + self.var_assign( + var.as_ref(), + true, + HashSet::from([GameValue::Number(None)]), + VarSource::ForLoop(source.clone()), + ); } - for for_stage in array { - let Expression::Code(for_statements) = for_stage else { - continue; - }; - self.code_used.insert(for_stage.clone()); - self.eval_statements(for_statements); + Expression::Code(stage_statement) => { + self.code_used.insert(stage.clone()); + self.eval_statements(stage_statement); } - } - None => { - do_run = false; - } - _ => { - unreachable!(""); + _ => {} } } + } else { + do_run = true; } } self.code_used.insert(expression.clone()); @@ -202,14 +204,35 @@ impl SciptScope { pub fn cmd_for(&mut self, rhs: &HashSet) -> HashSet { let mut return_value = HashSet::new(); for possible in rhs { + let mut possible_array = Vec::new(); match possible { - GameValue::Array(option) | GameValue::String(option) => { - return_value.insert(GameValue::ForType(option.clone())); + GameValue::String(option) => { + let Some(expression) = option else { + return_value.insert(GameValue::ForType(None)); + continue; + }; + possible_array.push(expression.clone()); } - _ => { - error!("shouldn't be reachable?"); - return_value.insert(GameValue::ForType(None)); + GameValue::Array(option) => { + let Some(for_stages) = option else { + return_value.insert(GameValue::ForType(None)); + continue; + }; + for stage in for_stages { + for gv in stage { + let GameValue::Code(Some(expression)) = gv else { + continue; + }; + possible_array.push(expression.clone()); + } + } } + _ => {} + } + if possible_array.is_empty() { + return_value.insert(GameValue::ForType(None)); + } else { + return_value.insert(GameValue::ForType(Some(possible_array))); } } return_value @@ -255,11 +278,15 @@ impl SciptScope { if let GameValue::Code(Some(Expression::Code(_statements))) = possible { return_value.extend(self.cmd_generic_call(rhs)); } - if let GameValue::Array(Some(Expression::Array(array, _))) = possible { - for expression in array { - return_value.extend(self.cmd_generic_call(&HashSet::from([GameValue::Code( - Some(expression.clone()), - )]))); + if let GameValue::Array(Some(gv_array)) = possible { + for gv_index in gv_array { + for element in gv_index { + if let GameValue::Code(Some(expression)) = element { + return_value.extend(self.cmd_generic_call(&HashSet::from([ + GameValue::Code(Some(expression.clone())), + ]))); + } + } } } } @@ -283,14 +310,14 @@ impl SciptScope { #[must_use] pub fn cmd_b_get_or_default_call(&mut self, rhs: &HashSet) -> HashSet { let mut possible_code = HashSet::new(); - for possible in rhs { - let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + for possible_outer in rhs { + let GameValue::Array(Some(gv_array)) = possible_outer else { continue; }; - if array.len() < 2 { + if gv_array.len() < 2 { continue; } - possible_code.insert(GameValue::Code(Some(array[1].clone()))); + possible_code.extend(gv_array[1].clone()); } let _ = self.cmd_generic_call(&possible_code); HashSet::from([GameValue::Anything]) diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index 311709fa1..1f0e92504 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -6,19 +6,18 @@ use super::{game_value::GameValue, SciptScope}; impl SciptScope { pub fn external_function(&mut self, lhs: &HashSet, rhs: &Expression) { - let Expression::Variable(var, _) = rhs else { + let Expression::Variable(ext_func, _) = rhs else { return; }; for possible in lhs { - let GameValue::Array(Some(Expression::Array(array, _))) = possible else { + let GameValue::Array(Some(gv_array)) = possible else { continue; }; - match var.to_ascii_lowercase().as_str() { + match ext_func.to_ascii_lowercase().as_str() { "cba_fnc_hasheachpair" | "cba_fnc_hashfilter" => { - if array.len() > 1 { - let code = self.eval_expression(&array[1]); + if gv_array.len() > 1 { self.external_current_scope( - &code, + &gv_array[1], &vec![ ("_key", GameValue::Anything), ("_value", GameValue::Anything), @@ -27,23 +26,28 @@ impl SciptScope { } } "cba_fnc_filter" => { - if array.len() > 1 { - let code = self.eval_expression(&array[1]); - self.external_current_scope(&code, &vec![("_x", GameValue::Anything)]); + if gv_array.len() > 1 { + self.external_current_scope( + &gv_array[1], + &vec![("_x", GameValue::Anything)], + ); } } "cba_fnc_directcall" => { - if !array.is_empty() { - let code = self.eval_expression(&array[0]); - self.external_current_scope(&code, &vec![]); + if !gv_array.is_empty() { + self.external_current_scope(&gv_array[0], &vec![]); + } + } + "ace_common_fnc_cachedcall" => { + if gv_array.len() > 1 { + self.external_current_scope(&gv_array[1], &vec![]); } } "ace_interact_menu_fnc_createaction" => { for index in 3..5 { - if array.len() > index { - let code = self.eval_expression(&array[index]); + if gv_array.len() > index { self.external_new_scope( - &code, + &gv_array[index], &vec![ ("_target", GameValue::Object), ("_player", GameValue::Object), @@ -52,16 +56,22 @@ impl SciptScope { } } } + "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" => { + if !gv_array.is_empty() { + self.external_new_scope(&gv_array[0], &vec![]); + } + } + "cba_fnc_addclasseventhandler" => { + if gv_array.len() > 2 { + self.external_new_scope(&gv_array[2], &vec![]); + } + } _ => {} } } } - fn external_new_scope( - &mut self, - possible_arg: &HashSet, - vars: &Vec<(&str, GameValue)>, - ) { - for element in possible_arg { + fn external_new_scope(&mut self, code_arg: &Vec, vars: &Vec<(&str, GameValue)>) { + for element in code_arg { let GameValue::Code(Some(expression)) = element else { continue; }; @@ -81,12 +91,8 @@ impl SciptScope { self.errors.extend(ext_scope.finish(false)); } } - fn external_current_scope( - &mut self, - possible_arg: &HashSet, - vars: &Vec<(&str, GameValue)>, - ) { - for element in possible_arg { + fn external_current_scope(&mut self, code_arg: &Vec, vars: &Vec<(&str, GameValue)>) { + for element in code_arg { let GameValue::Code(Some(expression)) = element else { continue; }; diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index d5a10407f..b24a49d68 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -9,14 +9,14 @@ use crate::{parser::database::Database, Expression}; pub enum GameValue { Anything, // Assignment, // as in z = call {x=1}??? - Array(Option), + Array(Option>>), Boolean(Option), Code(Option), Config, Control, DiaryRecord, Display, - ForType(Option), + ForType(Option>), Group, HashMap, IfType, @@ -51,6 +51,7 @@ impl GameValue { }; for syntax in command.syntax() { + // println!("syntax {:?}", syntax.call()); match syntax.call() { Call::Nular => { if !matches!(expression, Expression::NularCommand(..)) { @@ -60,7 +61,8 @@ impl GameValue { Call::Unary(rhs_arg) => { if !matches!(expression, Expression::UnaryCommand(..)) || !Self::match_set_to_arg( - rhs_set.expect("u args"), + cmd_name, + rhs_set.expect("unary rhs"), rhs_arg, syntax.params(), ) @@ -71,12 +73,14 @@ impl GameValue { Call::Binary(lhs_arg, rhs_arg) => { if !matches!(expression, Expression::BinaryCommand(..)) || !Self::match_set_to_arg( - lhs_set.expect("b args"), + cmd_name, + lhs_set.expect("binary lhs"), lhs_arg, syntax.params(), ) || !Self::match_set_to_arg( - rhs_set.expect("b args"), + cmd_name, + rhs_set.expect("binary rhs"), rhs_arg, syntax.params(), ) @@ -86,9 +90,12 @@ impl GameValue { } } let value = &syntax.ret().0; + // println!("match syntax {syntax:?}"); return_types.insert(Self::from_wiki_value(value)); } - // trace!( + // println!("lhs_set {lhs_set:?}"); + // println!("rhs_set {rhs_set:?}"); + // println!( // "cmd [{}] = {}:{:?}", // cmd_name, // return_types.len(), @@ -98,25 +105,83 @@ impl GameValue { } #[must_use] - pub fn match_set_to_arg(set: &HashSet, arg: &Arg, params: &[Param]) -> bool { + pub fn match_set_to_arg( + cmd_name: &str, + set: &HashSet, + arg: &Arg, + params: &[Param], + ) -> bool { match arg { Arg::Item(name) => { - // trace!("looking for {name} in {params:?}"); let Some(param) = params.iter().find(|p| p.name() == name) else { - warn!("param not found"); + /// Varadic cmds which will be missing wiki param matches + const WIKI_CMDS_IGNORE_MISSING_PARAM: &[&str] = &[ + "format", + "formatText", + "param", + "params", + "setGroupId", + "setGroupIdGlobal", + "set3DENMissionAttributes", + "setPiPEffect", + ]; + if !WIKI_CMDS_IGNORE_MISSING_PARAM.contains(&cmd_name) { + warn!("cmd {cmd_name} - param {name} not found"); + } return true; }; + // println!( + // "[arg {name}] typ: {:?}, opt: {:?}", + // param.typ(), + // param.optional() + // ); + if param.optional() { + // todo: maybe still check type if opt and not empty/nil? + return true; + } Self::match_set_to_value(set, param.typ()) } - Arg::Array(_vec_arg) => { - // todo: each individual array arg - Self::match_set_to_value(set, &Value::ArrayUnknown) + Arg::Array(arg_array) => { + const WIKI_CMDS_IGNORE_ARGS: &[&str] = &["createHashMapFromArray"]; + if WIKI_CMDS_IGNORE_ARGS.contains(&cmd_name) { + return true; + } + + let test = set.iter().any(|s| { + match s { + Self::Anything | Self::Array(None) => { + // println!("array (any/generic) pass"); + true + } + Self::Array(Some(gv_array)) => { + // println!("array (gv: {}) expected (arg: {})", gv_array.len(), arg_array.len()); + // if gv_array.len() > arg_array.len() { + // not really an error, some syntaxes take more than others + // } + for (index, arg) in arg_array.iter().enumerate() { + let possible = if index < gv_array.len() { + gv_array[index].iter().cloned().collect() + } else { + HashSet::new() + }; + if !Self::match_set_to_arg(cmd_name, &possible, arg, params) { + return false; + } + } + true + } + _ => false, + } + }); + + test } } } #[must_use] pub fn match_set_to_value(set: &HashSet, right_wiki: &Value) -> bool { + // println!("Checking {:?} against {:?}", set, right_wiki); let right = Self::from_wiki_value(right_wiki); set.iter().any(|gv| Self::match_values(gv, &right)) } @@ -189,9 +254,9 @@ impl GameValue { // format!("Assignment") // } Self::Anything => "Anything".to_string(), - Self::ForType(expression) => { - if let Some(Expression::String(str, _, _)) = expression { - format!("ForType(var {str})") + Self::ForType(for_args_array) => { + if for_args_array.is_some() { + format!("ForType(var {for_args_array:?})") } else { "ForType(GENERIC)".to_string() } @@ -217,9 +282,11 @@ impl GameValue { "Boolean(GENERIC)".to_string() } } - Self::Array(expression) => { - if let Some(Expression::Array(array, _)) = expression { - format!("ArrayExp(len {})", array.len()) + Self::Array(gv_array_option) => + { + #[allow(clippy::option_if_let_else)] + if let Some(gv_array) = gv_array_option { + format!("ArrayExp(len {})", gv_array.len()) } else { "ArrayExp(GENERIC)".to_string() } diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index e27ca3d55..68aa79d7d 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -24,30 +24,33 @@ mod game_value; pub enum Issue { InvalidArgs(String, Range), Undefined(String, Range, bool), - Unused(String, Range), + Unused(String, VarSource), Shadowed(String, Range), NotPrivate(String, Range), } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum VarSource { - Real(Range), + Assignment(Range), + ForLoop(Range), + Params(Range), + Private(Range), Magic(Range), Ignore, } impl VarSource { #[must_use] - pub const fn check_unused(&self) -> bool { - matches!(self, Self::Real(..)) - } - #[must_use] - pub const fn check_shadow(&self) -> bool { - matches!(self, Self::Real(..)) + pub const fn skip_errors(&self) -> bool { + matches!(self, Self::Magic(..)) || matches!(self, Self::Ignore) } #[must_use] pub fn get_range(&self) -> Option> { match self { - Self::Real(range) | Self::Magic(range) => Some(range.clone()), + Self::Assignment(range) + | Self::ForLoop(range) + | Self::Params(range) + | Self::Private(range) + | Self::Magic(range) => Some(range.clone()), Self::Ignore => None, } } @@ -129,10 +132,10 @@ impl SciptScope { pub fn pop(&mut self) { for (var, holder) in self.local.pop().unwrap_or_default() { // trace!("-- Stack Pop {}:{} ", var, holder.usage); - if holder.usage == 0 && holder.source.check_unused() { + if holder.usage == 0 && !holder.source.skip_errors() { self.errors.insert(Issue::Unused( var, - holder.source.get_range().unwrap_or_default(), + holder.source, )); } } @@ -172,7 +175,7 @@ impl SciptScope { } } else if local { // Only check shadowing inside the same scope-level (could make an option) - if source.check_shadow() && stack_level_search.unwrap_or_default() == 0 { + if stack_level_search.unwrap_or_default() == 0 && !source.skip_errors() { self.errors.insert(Issue::Shadowed( var.to_owned(), source.get_range().unwrap_or_default(), @@ -254,10 +257,11 @@ impl SciptScope { } Expression::String(..) => HashSet::from([GameValue::String(Some(expression.clone()))]), Expression::Array(array, _) => { - for e in array { - let _ = self.eval_expression(e); - } - HashSet::from([GameValue::Array(Some(expression.clone()))]) + let gv_array: Vec> = array + .iter() + .map(|e| self.eval_expression(e).into_iter().collect()) + .collect(); + HashSet::from([GameValue::Array(Some(gv_array))]) } Expression::NularCommand(cmd, source) => { debug_type = format!("[N:{}]", cmd.as_str()); @@ -394,13 +398,23 @@ impl SciptScope { Statement::AssignGlobal(var, expression, source) => { // x or _x let possible_values = self.eval_expression(expression); - self.var_assign(var, false, possible_values, VarSource::Real(source.clone())); + self.var_assign( + var, + false, + possible_values, + VarSource::Assignment(source.clone()), + ); // return_value = vec![GameValue::Assignment()]; } Statement::AssignLocal(var, expression, source) => { // private _x let possible_values = self.eval_expression(expression); - self.var_assign(var, true, possible_values, VarSource::Real(source.clone())); + self.var_assign( + var, + true, + possible_values, + VarSource::Assignment(source.clone()), + ); // return_value = vec![GameValue::Assignment()]; } Statement::Expression(expression, _) => { diff --git a/libs/sqf/src/analyze/lints/s12_inspector.rs b/libs/sqf/src/analyze/lints/s12_inspector.rs index 376ca1ae6..dde7772a0 100644 --- a/libs/sqf/src/analyze/lints/s12_inspector.rs +++ b/libs/sqf/src/analyze/lints/s12_inspector.rs @@ -1,6 +1,6 @@ use crate::{ analyze::{ - inspector::{self, Issue}, + inspector::{self, Issue, VarSource}, SqfLintData, }, Statements, @@ -90,7 +90,7 @@ impl LintRunner for Runner { if let Some(toml::Value::Boolean(b)) = config.option("check_child_scripts") { *b } else { - false // can cause false positives + true // can cause false positives }; let check_undefined = if let Some(toml::Value::Boolean(b)) = config.option("check_undefined") { @@ -109,13 +109,13 @@ impl LintRunner for Runner { if let Some(toml::Value::Boolean(b)) = config.option("check_unused") { *b } else { - false + true }; let check_shadow = if let Some(toml::Value::Boolean(b)) = config.option("check_shadow") { *b } else { - false + true }; let mut errors: Codes = Vec::new(); @@ -158,7 +158,8 @@ impl LintRunner for Runner { ))); } } - Issue::Unused(var, range) => { + Issue::Unused(var, source) => { + let VarSource::Assignment(range) = source else { continue }; if check_unused { errors.push(Arc::new(CodeS12Inspector::new( range, diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs index f6b0f74f7..5771aa632 100644 --- a/libs/sqf/tests/inspector.rs +++ b/libs/sqf/tests/inspector.rs @@ -14,10 +14,10 @@ fn get_statements(file: &str) -> (Processed, Statements, Arc) { let workspace = hemtt_workspace::Workspace::builder() .physical(&folder, LayerType::Source) .finish(None, false, &hemtt_common::config::PDriveOption::Disallow) - .unwrap(); - let source = workspace.join(file).unwrap(); - let processed = Processor::run(&source).unwrap(); - let statements = hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap(); + .expect("for test"); + let source = workspace.join(file).expect("for test"); + let processed = Processor::run(&source).expect("for test"); + let statements = hemtt_sqf::parser::run(&Database::a3(false), &processed).expect("for test"); let database = Arc::new(Database::a3(false)); (processed, statements, database) } @@ -25,7 +25,7 @@ fn get_statements(file: &str) -> (Processed, Statements, Arc) { #[cfg(test)] mod tests { use crate::get_statements; - use hemtt_sqf::analyze::inspector::{self, Issue}; + use hemtt_sqf::analyze::inspector::{self, Issue, VarSource}; #[test] pub fn test_0() { @@ -38,7 +38,7 @@ mod tests { pub fn test_1() { let (pro, sqf, database) = get_statements("test_1.sqf"); let result = inspector::run_processed(&sqf, &pro, &database, true); - assert_eq!(result.len(), 8); + assert_eq!(result.len(), 11); // Order not guarenteed assert!(result.iter().any(|i| { if let Issue::InvalidArgs(cmd, _) = i { @@ -49,28 +49,28 @@ mod tests { })); assert!(result.iter().any(|i| { if let Issue::Undefined(var, _, _) = i { - var == "_guy" + var == "_test2" } else { false } })); assert!(result.iter().any(|i| { if let Issue::NotPrivate(var, _) = i { - var == "_z" + var == "_test3" } else { false } })); assert!(result.iter().any(|i| { - if let Issue::Unused(var, _) = i { - var == "_c" + if let Issue::Unused(var, source) = i { + var == "_test4" && matches!(source, VarSource::Assignment(_)) } else { false } })); assert!(result.iter().any(|i| { if let Issue::Shadowed(var, _) = i { - var == "_var1" + var == "_test5" } else { false } @@ -91,10 +91,38 @@ mod tests { })); assert!(result.iter().any(|i| { if let Issue::Undefined(var, _, _) = i { - var == "_myLocalVar1" + var == "_test8" } else { false } })); + assert!(result.iter().any(|i| { + if let Issue::Undefined(var, _, _) = i { + var == "_test9" + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Unused(var, source) = i { + var == "_test10" && matches!(source, VarSource::ForLoop(_)) + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Unused(var, source) = i { + var == "_test11" && matches!(source, VarSource::Params(_)) + } else { + false + } + })); + } + + #[test] + pub fn test_2() { + let (pro, sqf, database) = get_statements("test_2.sqf"); + let result = inspector::run_processed(&sqf, &pro, &database, true); + assert_eq!(result.len(), 0); } } diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index b39b4ff27..2d68148c5 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -1,26 +1,28 @@ -x setFuel true; // args: takes number 0-1 -x setFuel f; -_guy setDamage 1; // _guy undefeind -private _you = player; -_you setDamage 0.5; -_z = 7; // not private -systemChat str _z; -private "_a"; -_a = 8; -private ["_b"]; -_b = _a + 1; -private _c = _b; // unused -params ["_var1"]; -private _var1 = 10; // shadow -diag_log text str _var1; +a setFuel b; +a setFuel 0; +a setFuel true; // invalidArgs: takes number 0-1 +_test2 setDamage 1; // undefiend +private _var1 = player; +_var1 setDamage 0.5; + +_test3 = 7; // not private +systemChat str _test3; +private "_var2"; +_var2 = 8; +private ["_var3"]; +_var3 = _var2 + 1; +private _test4 = _var3; // unused +params ["_test5"]; +private _test5 = 10; // shadow (same level) +diag_log text str _test5; gx = []; gx addPublicVariableEventHandler {}; // args: takes lhs string -for "_i" from 1 to 20 step 0.5 do { - systemChat str _i; +for "_var5" from 1 to 20 step 0.5 do { + systemChat str _var5; }; -for [{private _k = 0}, {_k < 5}, {_k = _k + 1}] do { - systemChat str _k; +for [{private _var6 = 0}, {_var6 < 5}, {_var6 = _var6 + 1}] do { + systemChat str _var6; }; //IGNORE_PRIVATE_WARNING["_fromUpper"]; @@ -29,11 +31,11 @@ X = _fromUpper; [] call { private "_weird"; //IGNORE_PRIVATE_WARNING["_weird"] - // No way to know the order is different - for "_i" from 1 to 5 do { - if (_i%2 == 0) then { + for "_var7" from 1 to 5 do { + if (_var7%2 == 0) then { truck lock _weird; }; - if (_i%2 == 1) then { + if (_var7%2 == 1) then { _weird = 0.5; }; }; @@ -47,10 +49,12 @@ if (y) then { setObjectViewDistance somePFEH; }; -private _condition = - { - [_player, _target] call y - }; +somehash getOrDefaultCall ["key", {_test8}, true]; //undefined +private _var8 = objNull; +somehash getOrDefaultCall ["key", {_var8}, true]; + +// Will have _player and _target +private _condition = { [_player, _target] call y }; [ "", localize "STR_A3_Arsenal", @@ -64,15 +68,25 @@ private _condition = private _hash = [] call CBA_fnc_hashCreate; private _dumpHash = { + // Will have _key and _value diag_log format ["Key: %1, Value: %2", _key, _value]; }; [_hash, _dumpHash] call CBA_fnc_hashEachPair; -private _myLocalVar1 = 555; -_myLocalVar1 = _myLocalVar1 + 1; +private _test9 = 555; +_test9= _test9 + 1; [{ - systemChat str _myLocalVar1; // invalid + systemChat str _test9; // invalid }, 0, []] call CBA_fnc_addPerFrameHandler; -private _myLocalVar2 = 55; -[{systemChat str _myLocalVar2}] call CBA_fnc_directCall; // fine +private _var9 = 55; +[{systemChat str _var9}] call CBA_fnc_directCall; + + // Will have _x +filter = [orig, {_x + 1}] call CBA_fnc_filter; + +private _var10 = 123; +[player, {x = _var10}] call ace_common_fnc_cachedcall; + +for "_test10" from 1 to 1 step 0.1 do {}; +[5] params ["_test11"]; diff --git a/libs/sqf/tests/inspector/test_2.sqf b/libs/sqf/tests/inspector/test_2.sqf new file mode 100644 index 000000000..de0855076 --- /dev/null +++ b/libs/sqf/tests/inspector/test_2.sqf @@ -0,0 +1,26 @@ +// Mainly checking wiki syntax for correct optionals + +// check inner nil +obj addWeaponItem ["weapon", ["item", nil, "muzzle"], true]; +obj addWeaponItem ["weapon", ["item"], true]; + + // check too many/few on variadic +format ["%1 %2 %3 %4 %5", 1, 2, 3, 4, 5]; +format [""]; +[] params []; + +// False positives on wiki +configProperties [configFile >> "ACE_Curator"]; +x selectionPosition [y, "Memory"]; +ropeCreate [obj1, pos1, objNull, [0, 0, 0], dist1]; +lineIntersectsSurfaces [[], [], objNull, objNull, true, 2]; +uuid insert [8, ["-"]]; +createTrigger["EmptyDetector", [1,2,3]]; +showHUD [true,false,false,false,false,false,false,true]; +createVehicle ["", [0,0,0]]; +x drawRectangle [getPos player, 20, 20, getDir player, [0,0,1,1],""]; + +createHashMapFromArray [["empty", {0}]]; +lineIntersectsObjs [eyePos player, ATLToASL screenToWorld [0.5, 0.5]]; +formatText ["%1%2%3", "line1", "
", "line2"]; +[] select [2]; From 8047f7cfa141db7c2daa8c72c92da7aa346b1c94 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 6 Oct 2024 03:01:31 -0500 Subject: [PATCH 04/24] Split-up lints --- libs/sqf/src/analyze/inspector/commands.rs | 75 ++++-- .../analyze/inspector/external_functions.rs | 159 +++++++---- libs/sqf/src/analyze/inspector/game_value.rs | 23 +- libs/sqf/src/analyze/inspector/mod.rs | 108 ++++---- libs/sqf/src/analyze/lints/s12_inspector.rs | 253 ------------------ .../sqf/src/analyze/lints/s12_invalid_args.rs | 135 ++++++++++ libs/sqf/src/analyze/lints/s13_undefined.rs | 149 +++++++++++ libs/sqf/src/analyze/lints/s14_unused.rs | 155 +++++++++++ libs/sqf/src/analyze/lints/s15_shadowed.rs | 136 ++++++++++ libs/sqf/src/analyze/lints/s16_not_private.rs | 135 ++++++++++ libs/sqf/src/lib.rs | 10 +- libs/sqf/src/parser/mod.rs | 5 +- libs/sqf/tests/inspector.rs | 30 ++- libs/sqf/tests/inspector/test_1.sqf | 70 +++-- libs/sqf/tests/lints.rs | 33 ++- libs/sqf/tests/lints/project_tests.toml | 13 +- libs/sqf/tests/lints/s12_inspector/source.sqf | 4 - .../sqf/tests/lints/s12_inspector/stdout.ansi | 13 - .../tests/lints/s12_invalid_args/source.sqf | 3 + .../tests/lints/s12_invalid_args/stdout.ansi | 6 + libs/sqf/tests/lints/s13_undefined/source.sqf | 1 + .../sqf/tests/lints/s13_undefined/stdout.ansi | 8 + libs/sqf/tests/lints/s14_unused/source.sqf | 2 + libs/sqf/tests/lints/s14_unused/stdout.ansi | 6 + libs/sqf/tests/lints/s15_shadowed/source.sqf | 4 + libs/sqf/tests/lints/s15_shadowed/stdout.ansi | 6 + .../tests/lints/s16_not_private/source.sqf | 3 + .../tests/lints/s16_not_private/stdout.ansi | 6 + 28 files changed, 1094 insertions(+), 457 deletions(-) delete mode 100644 libs/sqf/src/analyze/lints/s12_inspector.rs create mode 100644 libs/sqf/src/analyze/lints/s12_invalid_args.rs create mode 100644 libs/sqf/src/analyze/lints/s13_undefined.rs create mode 100644 libs/sqf/src/analyze/lints/s14_unused.rs create mode 100644 libs/sqf/src/analyze/lints/s15_shadowed.rs create mode 100644 libs/sqf/src/analyze/lints/s16_not_private.rs delete mode 100644 libs/sqf/tests/lints/s12_inspector/source.sqf delete mode 100644 libs/sqf/tests/lints/s12_inspector/stdout.ansi create mode 100644 libs/sqf/tests/lints/s12_invalid_args/source.sqf create mode 100644 libs/sqf/tests/lints/s12_invalid_args/stdout.ansi create mode 100644 libs/sqf/tests/lints/s13_undefined/source.sqf create mode 100644 libs/sqf/tests/lints/s13_undefined/stdout.ansi create mode 100644 libs/sqf/tests/lints/s14_unused/source.sqf create mode 100644 libs/sqf/tests/lints/s14_unused/stdout.ansi create mode 100644 libs/sqf/tests/lints/s15_shadowed/source.sqf create mode 100644 libs/sqf/tests/lints/s15_shadowed/stdout.ansi create mode 100644 libs/sqf/tests/lints/s16_not_private/source.sqf create mode 100644 libs/sqf/tests/lints/s16_not_private/stdout.ansi diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index da6911be4..88a443bd6 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, ops::Range}; -use crate::{analyze::inspector::VarSource, Expression}; +use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; use super::{game_value::GameValue, SciptScope}; @@ -97,7 +97,11 @@ impl SciptScope { HashSet::from([GameValue::Boolean(None)]) } #[must_use] - pub fn cmd_generic_call(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_generic_call( + &mut self, + rhs: &HashSet, + database: &Database, + ) -> HashSet { for possible in rhs { let GameValue::Code(Some(expression)) = possible else { continue; @@ -110,7 +114,7 @@ impl SciptScope { } self.push(); self.code_used.insert(expression.clone()); - self.eval_statements(statements); + self.eval_statements(statements, database); self.pop(); } HashSet::from([GameValue::Anything]) @@ -120,6 +124,7 @@ impl SciptScope { &mut self, lhs: &HashSet, rhs: &HashSet, + database: &Database, ) -> HashSet { for possible in rhs { let GameValue::Code(Some(expression)) = possible else { @@ -151,7 +156,7 @@ impl SciptScope { } Expression::Code(stage_statement) => { self.code_used.insert(stage.clone()); - self.eval_statements(stage_statement); + self.eval_statements(stage_statement, database); } _ => {} } @@ -162,7 +167,7 @@ impl SciptScope { } self.code_used.insert(expression.clone()); if do_run { - self.eval_statements(statements); + self.eval_statements(statements, database); } self.pop(); } @@ -172,8 +177,9 @@ impl SciptScope { pub fn cmd_generic_call_magic( &mut self, code_possibilities: &HashSet, - magic: &Vec, + magic: &Vec<&str>, source: &Range, + database: &Database, ) -> HashSet { for possible in code_possibilities { let GameValue::Code(Some(expression)) = possible else { @@ -195,7 +201,7 @@ impl SciptScope { ); } self.code_used.insert(expression.clone()); - self.eval_statements(statements); + self.eval_statements(statements, database); self.pop(); } HashSet::from([GameValue::Anything]) @@ -247,7 +253,11 @@ impl SciptScope { lhs.clone() } #[must_use] - pub fn cmd_u_is_nil(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_u_is_nil( + &mut self, + rhs: &HashSet, + database: &Database, + ) -> HashSet { let mut non_string = false; for possible in rhs { let GameValue::String(possible) = possible else { @@ -263,7 +273,7 @@ impl SciptScope { let _ = self.var_retrieve(var, &expression.span(), true); } if non_string { - let _ = self.cmd_generic_call(rhs); + let _ = self.cmd_generic_call(rhs, database); } HashSet::from([GameValue::Boolean(None)]) } @@ -272,19 +282,21 @@ impl SciptScope { &mut self, _lhs: &HashSet, rhs: &HashSet, + database: &Database, ) -> HashSet { let mut return_value = HashSet::new(); for possible in rhs { if let GameValue::Code(Some(Expression::Code(_statements))) = possible { - return_value.extend(self.cmd_generic_call(rhs)); + return_value.extend(self.cmd_generic_call(rhs, database)); } if let GameValue::Array(Some(gv_array)) = possible { for gv_index in gv_array { for element in gv_index { if let GameValue::Code(Some(expression)) = element { - return_value.extend(self.cmd_generic_call(&HashSet::from([ - GameValue::Code(Some(expression.clone())), - ]))); + return_value.extend(self.cmd_generic_call( + &HashSet::from([GameValue::Code(Some(expression.clone()))]), + database, + )); } } } @@ -308,7 +320,11 @@ impl SciptScope { return_value } #[must_use] - pub fn cmd_b_get_or_default_call(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_b_get_or_default_call( + &mut self, + rhs: &HashSet, + database: &Database, + ) -> HashSet { let mut possible_code = HashSet::new(); for possible_outer in rhs { let GameValue::Array(Some(gv_array)) = possible_outer else { @@ -319,7 +335,7 @@ impl SciptScope { } possible_code.extend(gv_array[1].clone()); } - let _ = self.cmd_generic_call(&possible_code); + let _ = self.cmd_generic_call(&possible_code, database); HashSet::from([GameValue::Anything]) } #[must_use] @@ -336,4 +352,33 @@ impl SciptScope { } HashSet::from([GameValue::String(None)]) } + #[must_use] + pub fn cmd_b_select( + &mut self, + lhs: &HashSet, + rhs: &HashSet, + cmd_set: &HashSet, + source: &Range, + database: &Database, + ) -> HashSet { + let mut return_value = cmd_set.clone(); + // Check: `array select expression` + let _ = self.cmd_generic_call_magic(rhs, &vec!["_x"], source, database); + // if lhs is array, and rhs is bool/number then put array into return + if lhs.len() == 1 + && rhs + .iter() + .any(|r| matches!(r, GameValue::Boolean(..)) || matches!(r, GameValue::Number(..))) + { + if let Some(GameValue::Array(Some(gv_array))) = lhs.iter().next() { + // return_value.clear(); // todo: could clear if we handle pushBack + for gv_index in gv_array { + for element in gv_index { + return_value.insert(element.clone()); + } + } + } + } + return_value + } } diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index 1f0e92504..e7d6f1cbb 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -1,76 +1,128 @@ use std::collections::HashSet; -use crate::{analyze::inspector::VarSource, Expression}; +use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; use super::{game_value::GameValue, SciptScope}; impl SciptScope { - pub fn external_function(&mut self, lhs: &HashSet, rhs: &Expression) { + #[allow(clippy::too_many_lines)] + pub fn external_function( + &mut self, + lhs: &HashSet, + rhs: &Expression, + database: &Database, + ) { let Expression::Variable(ext_func, _) = rhs else { return; }; for possible in lhs { - let GameValue::Array(Some(gv_array)) = possible else { - continue; - }; - match ext_func.to_ascii_lowercase().as_str() { - "cba_fnc_hasheachpair" | "cba_fnc_hashfilter" => { - if gv_array.len() > 1 { - self.external_current_scope( - &gv_array[1], - &vec![ - ("_key", GameValue::Anything), - ("_value", GameValue::Anything), - ], - ); - } - } - "cba_fnc_filter" => { - if gv_array.len() > 1 { + match possible { + GameValue::Code(Some(statements)) => { + // handle `{} call cba_fnc_directcall` + if ext_func.to_ascii_lowercase().as_str() == "cba_fnc_directcall" { self.external_current_scope( - &gv_array[1], - &vec![("_x", GameValue::Anything)], + &vec![GameValue::Code(Some(statements.clone()))], + &vec![], + database, ); } } - "cba_fnc_directcall" => { - if !gv_array.is_empty() { - self.external_current_scope(&gv_array[0], &vec![]); + GameValue::Array(Some(gv_array)) => match ext_func.to_ascii_lowercase().as_str() { + // Functions that will run in existing scope + "cba_fnc_hasheachpair" | "cba_fnc_hashfilter" => { + if gv_array.len() > 1 { + self.external_current_scope( + &gv_array[1], + &vec![ + ("_key", GameValue::Anything), + ("_value", GameValue::Anything), + ], + database, + ); + } } - } - "ace_common_fnc_cachedcall" => { - if gv_array.len() > 1 { - self.external_current_scope(&gv_array[1], &vec![]); + "cba_fnc_filter" => { + if gv_array.len() > 1 { + self.external_current_scope( + &gv_array[1], + &vec![("_x", GameValue::Anything)], + database, + ); + } } - } - "ace_interact_menu_fnc_createaction" => { - for index in 3..5 { - if gv_array.len() > index { - self.external_new_scope( - &gv_array[index], + "cba_fnc_inject" => { + if gv_array.len() > 2 { + self.external_current_scope( + &gv_array[2], &vec![ - ("_target", GameValue::Object), - ("_player", GameValue::Object), + ("_x", GameValue::Anything), + ("_accumulator", GameValue::Anything), ], + database, ); } } - } - "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" => { - if !gv_array.is_empty() { - self.external_new_scope(&gv_array[0], &vec![]); + "cba_fnc_directcall" => { + if !gv_array.is_empty() { + self.external_current_scope(&gv_array[0], &vec![], database); + } } - } - "cba_fnc_addclasseventhandler" => { - if gv_array.len() > 2 { - self.external_new_scope(&gv_array[2], &vec![]); + "ace_common_fnc_cachedcall" => { + if gv_array.len() > 1 { + self.external_current_scope(&gv_array[1], &vec![], database); + } } - } + // Functions that will start in a new scope + "ace_interact_menu_fnc_createaction" => { + for index in 3..=5 { + if gv_array.len() > index { + self.external_new_scope( + &gv_array[index], + &vec![ + ("_target", GameValue::Object), + ("_player", GameValue::Object), + ], + database, + ); + } + } + } + "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" => { + if !gv_array.is_empty() { + self.external_new_scope(&gv_array[0], &vec![], database); + } + } + "cba_fnc_addclasseventhandler" => { + if gv_array.len() > 2 { + self.external_new_scope(&gv_array[2], &vec![], database); + } + } + "cba_fnc_addbiseventhandler" => { + if gv_array.len() > 2 { + self.external_new_scope( + &gv_array[2], + &vec![ + ("_thisType", GameValue::String(None)), + ("_thisId", GameValue::Number(None)), + ("_thisFnc", GameValue::Code(None)), + ("_thisArgs", GameValue::Anything), + ], + database, + ); + } + } + _ => {} + }, _ => {} } } } - fn external_new_scope(&mut self, code_arg: &Vec, vars: &Vec<(&str, GameValue)>) { + fn external_new_scope( + &mut self, + code_arg: &Vec, + vars: &Vec<(&str, GameValue)>, + database: &Database, + ) { for element in code_arg { let GameValue::Code(Some(expression)) = element else { continue; @@ -81,17 +133,22 @@ impl SciptScope { if self.code_used.contains(expression) { return; } - let mut ext_scope = Self::create(&self.ignored_vars, &self.database, true); + let mut ext_scope = Self::create(&self.ignored_vars, true); for (var, value) in vars { ext_scope.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); } self.code_used.insert(expression.clone()); - ext_scope.eval_statements(statements); - self.errors.extend(ext_scope.finish(false)); + ext_scope.eval_statements(statements, database); + self.errors.extend(ext_scope.finish(false, database)); } } - fn external_current_scope(&mut self, code_arg: &Vec, vars: &Vec<(&str, GameValue)>) { + fn external_current_scope( + &mut self, + code_arg: &Vec, + vars: &Vec<(&str, GameValue)>, + database: &Database, + ) { for element in code_arg { let GameValue::Code(Some(expression)) = element else { continue; @@ -107,7 +164,7 @@ impl SciptScope { self.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); } self.code_used.insert(expression.clone()); - self.eval_statements(statements); + self.eval_statements(statements, database); self.pop(); } } diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index b24a49d68..080aae781 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; use arma3_wiki::model::{Arg, Call, Param, Value}; use tracing::{trace, warn}; @@ -37,16 +37,17 @@ pub enum GameValue { impl GameValue { #[must_use] + /// Gets cmd return types based on input types pub fn from_cmd( expression: &Expression, lhs_set: Option<&HashSet>, rhs_set: Option<&HashSet>, - database: &Arc, + database: &Database, ) -> HashSet { let mut return_types = HashSet::new(); let cmd_name = expression.command_name().expect("has a name"); let Some(command) = database.wiki().commands().get(cmd_name) else { - trace!("cmd {cmd_name} not in db?"); //ToDo: this can't find short cmds like &&, || + println!("cmd {cmd_name} not in db?"); return HashSet::from([Self::Anything]); }; @@ -124,6 +125,7 @@ impl GameValue { "setGroupIdGlobal", "set3DENMissionAttributes", "setPiPEffect", + "ppEffectCreate", ]; if !WIKI_CMDS_IGNORE_MISSING_PARAM.contains(&cmd_name) { warn!("cmd {cmd_name} - param {name} not found"); @@ -135,11 +137,7 @@ impl GameValue { // param.typ(), // param.optional() // ); - if param.optional() { - // todo: maybe still check type if opt and not empty/nil? - return true; - } - Self::match_set_to_value(set, param.typ()) + Self::match_set_to_value(set, param.typ(), param.optional()) } Arg::Array(arg_array) => { const WIKI_CMDS_IGNORE_ARGS: &[&str] = &["createHashMapFromArray"]; @@ -180,8 +178,11 @@ impl GameValue { } #[must_use] - pub fn match_set_to_value(set: &HashSet, right_wiki: &Value) -> bool { - // println!("Checking {:?} against {:?}", set, right_wiki); + pub fn match_set_to_value(set: &HashSet, right_wiki: &Value, optional: bool) -> bool { + // println!("Checking {:?} against {:?} [O:{optional}]", set, right_wiki); + if optional && (set.is_empty() || set.contains(&Self::Nothing)) { + return true; + } let right = Self::from_wiki_value(right_wiki); set.iter().any(|gv| Self::match_values(gv, &right)) } @@ -203,7 +204,7 @@ impl GameValue { /// Maps from Wiki:Value to Inspector:GameValue pub fn from_wiki_value(value: &Value) -> Self { match value { - Value::Anything => Self::Anything, + Value::Anything | Value::EdenEntity => Self::Anything, Value::ArrayColor | Value::ArrayColorRgb | Value::ArrayColorRgba diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 68aa79d7d..1387e6828 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -4,7 +4,6 @@ use std::{ collections::{HashMap, HashSet}, hash::Hash, ops::Range, - sync::Arc, vec, }; @@ -66,32 +65,26 @@ pub struct VarHolder { pub type Stack = HashMap; pub struct SciptScope { - database: Arc, errors: HashSet, global: Stack, local: Vec, code_seen: HashSet, code_used: HashSet, - is_child: bool, + is_orphan_scope: bool, ignored_vars: HashSet, } impl SciptScope { #[must_use] - pub fn create( - ignored_vars: &HashSet, - database: &Arc, - is_child: bool, - ) -> Self { + pub fn create(ignored_vars: &HashSet, is_orphan_scope: bool) -> Self { // trace!("Creating ScriptScope"); let mut scope = Self { - database: database.clone(), errors: HashSet::new(), global: Stack::new(), local: Vec::new(), code_seen: HashSet::new(), code_used: HashSet::new(), - is_child, + is_orphan_scope, ignored_vars: ignored_vars.clone(), }; scope.push(); @@ -106,7 +99,7 @@ impl SciptScope { scope } #[must_use] - pub fn finish(&mut self, check_child_scripts: bool) -> HashSet { + pub fn finish(&mut self, check_child_scripts: bool, database: &Database) -> HashSet { self.pop(); if check_child_scripts { let unused = &self.code_seen - &self.code_used; @@ -116,10 +109,10 @@ impl SciptScope { continue; }; // trace!("-- Checking external scope"); - let mut external_scope = Self::create(&self.ignored_vars, &self.database, true); - external_scope.eval_statements(&statements); + let mut external_scope = Self::create(&self.ignored_vars, true); + external_scope.eval_statements(&statements, database); self.errors - .extend(external_scope.finish(check_child_scripts)); + .extend(external_scope.finish(check_child_scripts, database)); } } self.errors.clone() @@ -133,10 +126,7 @@ impl SciptScope { for (var, holder) in self.local.pop().unwrap_or_default() { // trace!("-- Stack Pop {}:{} ", var, holder.usage); if holder.usage == 0 && !holder.source.skip_errors() { - self.errors.insert(Issue::Unused( - var, - holder.source, - )); + self.errors.insert(Issue::Unused(var, holder.source)); } } } @@ -215,7 +205,7 @@ impl SciptScope { self.errors.insert(Issue::Undefined( var.to_owned(), source.clone(), - self.is_child, + self.is_orphan_scope, )); } } else { @@ -247,7 +237,11 @@ impl SciptScope { #[must_use] #[allow(clippy::too_many_lines)] /// Evaluate expression in current scope - pub fn eval_expression(&mut self, expression: &Expression) -> HashSet { + pub fn eval_expression( + &mut self, + expression: &Expression, + database: &Database, + ) -> HashSet { let mut debug_type = String::new(); let possible_values = match expression { Expression::Variable(var, source) => self.var_retrieve(var, source, false), @@ -259,13 +253,13 @@ impl SciptScope { Expression::Array(array, _) => { let gv_array: Vec> = array .iter() - .map(|e| self.eval_expression(e).into_iter().collect()) + .map(|e| self.eval_expression(e, database).into_iter().collect()) .collect(); HashSet::from([GameValue::Array(Some(gv_array))]) } Expression::NularCommand(cmd, source) => { debug_type = format!("[N:{}]", cmd.as_str()); - let cmd_set = GameValue::from_cmd(expression, None, None, &self.database); + let cmd_set = GameValue::from_cmd(expression, None, None, database); if cmd_set.is_empty() { // is this possible? self.errors @@ -275,8 +269,8 @@ impl SciptScope { } Expression::UnaryCommand(cmd, rhs, source) => { debug_type = format!("[U:{}]", cmd.as_str()); - let rhs_set = self.eval_expression(rhs); - let cmd_set = GameValue::from_cmd(expression, None, Some(&rhs_set), &self.database); + let rhs_set = self.eval_expression(rhs, database); + let cmd_set = GameValue::from_cmd(expression, None, Some(&rhs_set), database); if cmd_set.is_empty() { self.errors .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); @@ -285,10 +279,10 @@ impl SciptScope { UnaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { "params" => Some(self.cmd_generic_params(&rhs_set)), "private" => Some(self.cmd_u_private(&rhs_set)), - "call" => Some(self.cmd_generic_call(&rhs_set)), - "isnil" => Some(self.cmd_u_is_nil(&rhs_set)), + "call" => Some(self.cmd_generic_call(&rhs_set, database)), + "isnil" => Some(self.cmd_u_is_nil(&rhs_set, database)), "while" | "waituntil" | "default" => { - let _ = self.cmd_generic_call(&rhs_set); + let _ = self.cmd_generic_call(&rhs_set, database); None } "for" => Some(self.cmd_for(&rhs_set)), @@ -302,10 +296,10 @@ impl SciptScope { } Expression::BinaryCommand(cmd, lhs, rhs, source) => { debug_type = format!("[B:{}]", cmd.as_str()); - let lhs_set = self.eval_expression(lhs); - let rhs_set = self.eval_expression(rhs); + let lhs_set = self.eval_expression(lhs, database); + let rhs_set = self.eval_expression(rhs, database); let cmd_set = - GameValue::from_cmd(expression, Some(&lhs_set), Some(&rhs_set), &self.database); + GameValue::from_cmd(expression, Some(&lhs_set), Some(&rhs_set), database); if cmd_set.is_empty() { // we must have invalid args self.errors @@ -314,57 +308,60 @@ impl SciptScope { let return_set = match cmd { BinaryCommand::Associate => { // the : from case - let _ = self.cmd_generic_call(&rhs_set); + let _ = self.cmd_generic_call(&rhs_set, database); None } BinaryCommand::And | BinaryCommand::Or => { - let _ = self.cmd_generic_call(&rhs_set); + let _ = self.cmd_generic_call(&rhs_set, database); None } BinaryCommand::Else => Some(self.cmd_b_else(&lhs_set, &rhs_set)), BinaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { "params" => Some(self.cmd_generic_params(&rhs_set)), "call" => { - self.external_function(&lhs_set, rhs); - Some(self.cmd_generic_call(&rhs_set)) + self.external_function(&lhs_set, rhs, database); + Some(self.cmd_generic_call(&rhs_set, database)) } "exitwith" => { // todo: handle scope exits - Some(self.cmd_generic_call(&rhs_set)) + Some(self.cmd_generic_call(&rhs_set, database)) } "do" => { // from While, With, For, and Switch - Some(self.cmd_b_do(&lhs_set, &rhs_set)) + Some(self.cmd_b_do(&lhs_set, &rhs_set, database)) } "from" | "to" | "step" => Some(self.cmd_b_from_chain(&lhs_set, &rhs_set)), - "then" => Some(self.cmd_b_then(&lhs_set, &rhs_set)), + "then" => Some(self.cmd_b_then(&lhs_set, &rhs_set, database)), "foreach" | "foreachreversed" => Some(self.cmd_generic_call_magic( &lhs_set, - &vec![ - "_x".to_string(), - "_y".to_string(), - "_forEachIndex".to_string(), - ], + &vec!["_x", "_y", "_forEachIndex"], source, + database, )), "count" => { let _ = self.cmd_generic_call_magic( &lhs_set, - &vec!["_x".to_string()], + &vec!["_x"], source, + database, ); None } - "findif" | "apply" | "select" => { - //todo handle (array select number) or (string select [1,2]); + "findif" | "apply" => { let _ = self.cmd_generic_call_magic( &rhs_set, - &vec!["_x".to_string()], + &vec!["_x"], source, + database, ); None } - "getordefaultcall" => Some(self.cmd_b_get_or_default_call(&rhs_set)), + "getordefaultcall" => { + Some(self.cmd_b_get_or_default_call(&rhs_set, database)) + } + "select" => { + Some(self.cmd_b_select(&lhs_set, &rhs_set, &cmd_set, source, database)) + } _ => None, }, _ => None, @@ -391,13 +388,13 @@ impl SciptScope { } /// Evaluate statements in the current scope - fn eval_statements(&mut self, statements: &Statements) { + fn eval_statements(&mut self, statements: &Statements, database: &Database) { // let mut return_value = HashSet::new(); for statement in statements.content() { match statement { Statement::AssignGlobal(var, expression, source) => { // x or _x - let possible_values = self.eval_expression(expression); + let possible_values = self.eval_expression(expression, database); self.var_assign( var, false, @@ -408,7 +405,7 @@ impl SciptScope { } Statement::AssignLocal(var, expression, source) => { // private _x - let possible_values = self.eval_expression(expression); + let possible_values = self.eval_expression(expression, database); self.var_assign( var, true, @@ -418,7 +415,7 @@ impl SciptScope { // return_value = vec![GameValue::Assignment()]; } Statement::Expression(expression, _) => { - let _possible_values = self.eval_expression(expression); + let _possible_values = self.eval_expression(expression, database); // return_value = possible_values; } } @@ -432,8 +429,7 @@ impl SciptScope { pub fn run_processed( statements: &Statements, processed: &Processed, - database: &Arc, - check_child_scripts: bool, + database: &Database, ) -> Vec { let mut ignored_vars = HashSet::new(); ignored_vars.insert("_this".to_string()); @@ -451,7 +447,7 @@ pub fn run_processed( } } - let mut scope = SciptScope::create(&ignored_vars, database, false); - scope.eval_statements(statements); - scope.finish(check_child_scripts).into_iter().collect() + let mut scope = SciptScope::create(&ignored_vars, false); + scope.eval_statements(statements, database); + scope.finish(true, database).into_iter().collect() } diff --git a/libs/sqf/src/analyze/lints/s12_inspector.rs b/libs/sqf/src/analyze/lints/s12_inspector.rs deleted file mode 100644 index dde7772a0..000000000 --- a/libs/sqf/src/analyze/lints/s12_inspector.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::{ - analyze::{ - inspector::{self, Issue, VarSource}, - SqfLintData, - }, - 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}; -use tracing::trace; - -crate::lint!(LintS12Inspector); - -impl Lint for LintS12Inspector { - fn ident(&self) -> &str { - "inspector" - } - - fn sort(&self) -> u32 { - 120 - } - - fn description(&self) -> &str { - "Checks for code usage" - } - - fn documentation(&self) -> &str { -r"### Configuration -- **check_invalid_args**: [default: true] check_invalid_args (e.g. `x setFuel true`) -- **check_child_scripts**: [default: false] Checks oprhaned scripts. - Assumes un-called code will run in another scope (can cause false positives) - e.g. `private _var = 5; [{_var}] call CBA_fnc_addPerFrameEventHandler;` -- **check_undefined**: [default: true] Checks local vars that are not defined -- **check_not_private**: [default: true] Checks local vars that are not `private` -- **check_unused**: [default: false] Checks local vars that are never used -- **check_shadow**: [default: false] Checks local vars that are shaddowed - -```toml -[lints.sqf.inspector] -options.check_invalid_args = true -options.check_child_scripts = true -options.check_undefined = true -options.check_not_private = true -options.check_unused = true -options.check_shadow = true -```" - } - - fn default_config(&self) -> LintConfig { - LintConfig::help() - } - - fn runners(&self) -> Vec>> { - vec![Box::new(Runner)] - } -} - -pub struct Runner; -impl LintRunner for Runner { - type Target = Statements; - #[allow(clippy::too_many_lines)] - fn run( - &self, - _project: Option<&hemtt_common::config::ProjectConfig>, - config: &hemtt_common::config::LintConfig, - processed: Option<&hemtt_workspace::reporting::Processed>, - target: &Statements, - data: &SqfLintData, - ) -> hemtt_workspace::reporting::Codes { - let Some(processed) = processed else { - return Vec::new(); - }; - if !target.top_level { - // we only want to handle full files, not all the sub-statements - return Vec::new(); - }; - let (_addon, database) = data; - - let check_invalid_args = - if let Some(toml::Value::Boolean(b)) = config.option("check_invalid_args") { - *b - } else { - true - }; - let check_child_scripts = - if let Some(toml::Value::Boolean(b)) = config.option("check_child_scripts") { - *b - } else { - true // can cause false positives - }; - let check_undefined = - if let Some(toml::Value::Boolean(b)) = config.option("check_undefined") { - *b - } else { - true - }; - let check_not_private = if let Some(toml::Value::Boolean(b)) = - config.option("check_not_private") - { - *b - } else { - true - }; - let check_unused = - if let Some(toml::Value::Boolean(b)) = config.option("check_unused") { - *b - } else { - true - }; - let check_shadow = - if let Some(toml::Value::Boolean(b)) = config.option("check_shadow") { - *b - } else { - true - }; - - let mut errors: Codes = Vec::new(); - let issues = inspector::run_processed(target, processed, database, check_child_scripts); - trace!("issues {}", issues.len()); - - for issue in issues { - match issue { - Issue::InvalidArgs(cmd, range) => { - if check_invalid_args { - errors.push(Arc::new(CodeS12Inspector::new( - range, - format!("Bad Args: {cmd}"), - None, - config.severity(), - processed, - ))); - } - } - Issue::Undefined(var, range, is_child) => { - if check_undefined { - let error_hint = if is_child {Some("From Child Code - may not be a problem".to_owned())} else {None}; - errors.push(Arc::new(CodeS12Inspector::new( - range, - format!("Undefined: {var}"), - error_hint, - config.severity(), - processed, - ))); - } - } - Issue::NotPrivate(var, range) => { - if check_not_private { - errors.push(Arc::new(CodeS12Inspector::new( - range, - format!("NotPrivate: {var}"), - None, - config.severity(), - processed, - ))); - } - } - Issue::Unused(var, source) => { - let VarSource::Assignment(range) = source else { continue }; - if check_unused { - errors.push(Arc::new(CodeS12Inspector::new( - range, - format!("Unused: {var}"), - None, - config.severity(), - processed, - ))); - } - } - Issue::Shadowed(var, range) => { - if check_shadow { - errors.push(Arc::new(CodeS12Inspector::new( - range, - format!("Shadowed: {var}"), - None, - config.severity(), - processed, - ))); - } - } - }; - } - - errors - } -} - -#[allow(clippy::module_name_repetitions)] -pub struct CodeS12Inspector { - span: Range, - error_type: String, - error_hint: Option, - severity: Severity, - diagnostic: Option, -} - -impl Code for CodeS12Inspector { - fn ident(&self) -> &'static str { - "L-S12" - } - - fn link(&self) -> Option<&str> { - Some("/analysis/sqf.html#inspector") - } - /// Top message - fn message(&self) -> String { - format!("Inspector - {}", self.error_type) - } - /// Under ^^^span hint - fn label_message(&self) -> String { - String::new() - } - /// bottom note - fn note(&self) -> Option { - self.error_hint.clone() - } - - fn severity(&self) -> Severity { - self.severity - } - - fn diagnostic(&self) -> Option { - self.diagnostic.clone() - } -} - -impl CodeS12Inspector { - #[must_use] - pub fn new( - span: Range, - error_type: String, - error_hint: Option, - severity: Severity, - processed: &Processed, - ) -> Self { - Self { - span, - error_type, - error_hint, - severity, - diagnostic: None, - } - .generate_processed(processed) - } - - fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); - self - } -} diff --git a/libs/sqf/src/analyze/lints/s12_invalid_args.rs b/libs/sqf/src/analyze/lints/s12_invalid_args.rs new file mode 100644 index 000000000..a535a18a2 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s12_invalid_args.rs @@ -0,0 +1,135 @@ +use crate::{ + analyze::{inspector::Issue, SqfLintData}, + 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::lint!(LintS12InvalidArgs); + +impl Lint for LintS12InvalidArgs { + fn ident(&self) -> &str { + "invalid_args" + } + fn sort(&self) -> u32 { + 120 + } + fn description(&self) -> &str { + "Invalid Args" + } + fn documentation(&self) -> &str { + r"### Example + +**Incorrect** +```sqf +(vehicle player) setFuel true; // bad args: takes number 0-1 +``` + +### Explanation + +Checks correct syntax usage." + } + fn default_config(&self) -> LintConfig { + LintConfig::help() + } + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner 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: &SqfLintData, + ) -> 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::InvalidArgs(cmd, range) = issue { + errors.push(Arc::new(CodeS12InvalidArgs::new( + range.to_owned(), + cmd.to_owned(), + None, + config.severity(), + processed, + ))); + } + } + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS12InvalidArgs { + span: Range, + token_name: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS12InvalidArgs { + fn ident(&self) -> &'static str { + "L-S12" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#invalid_args") + } + /// Top message + fn message(&self) -> String { + format!("Invalid Args - {}", self.token_name) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + fn severity(&self) -> Severity { + self.severity + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS12InvalidArgs { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + token_name: error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/analyze/lints/s13_undefined.rs b/libs/sqf/src/analyze/lints/s13_undefined.rs new file mode 100644 index 000000000..01ac0a122 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s13_undefined.rs @@ -0,0 +1,149 @@ +use crate::{ + analyze::{inspector::Issue, SqfLintData}, + 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::lint!(LintS13Undefined); + +impl Lint for LintS13Undefined { + fn ident(&self) -> &str { + "undefined" + } + fn sort(&self) -> u32 { + 130 + } + fn description(&self) -> &str { + "Undefined Variable" + } + fn documentation(&self) -> &str { + r"### Example + +**Incorrect** +```sqf +systemChat _neverDefined; +``` + +### Explanation + +Checks correct syntax usage." + } + fn default_config(&self) -> LintConfig { + LintConfig::help() + } + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner 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: &SqfLintData, + ) -> hemtt_workspace::reporting::Codes { + if target.issues().is_empty() { + return Vec::new(); + }; + let Some(processed) = processed else { + return Vec::new(); + }; + let check_oprhan_code = + if let Some(toml::Value::Boolean(b)) = config.option("check_oprhan_code") { + *b + } else { + false + }; + let mut errors: Codes = Vec::new(); + for issue in target.issues() { + if let Issue::Undefined(var, range, is_orphan_scope) = issue { + let error_hint = if *is_orphan_scope { + if !check_oprhan_code { + continue; + } + Some("From Orphan Code - may not be a problem".to_owned()) + } else { + None + }; + errors.push(Arc::new(CodeS13Undefined::new( + range.to_owned(), + var.to_owned(), + error_hint, + config.severity(), + processed, + ))); + } + } + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS13Undefined { + span: Range, + token_name: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS13Undefined { + fn ident(&self) -> &'static str { + "L-S13" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#undefined") + } + /// Top message + fn message(&self) -> String { + format!("Undefined Var - {}", self.token_name) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + fn severity(&self) -> Severity { + self.severity + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS13Undefined { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + token_name: error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/analyze/lints/s14_unused.rs b/libs/sqf/src/analyze/lints/s14_unused.rs new file mode 100644 index 000000000..a35e3ae7c --- /dev/null +++ b/libs/sqf/src/analyze/lints/s14_unused.rs @@ -0,0 +1,155 @@ +use crate::{ + analyze::{ + inspector::{Issue, VarSource}, + SqfLintData, + }, + 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::lint!(LintS14Unused); + +impl Lint for LintS14Unused { + fn ident(&self) -> &str { + "unused" + } + fn sort(&self) -> u32 { + 120 + } + fn description(&self) -> &str { + "Unused Var" + } + fn documentation(&self) -> &str { + r"### Example + +**Incorrect** +```sqf +private _z = 5; // and never used +``` + +### Explanation + +Checks for vars that are never used." + } + fn default_config(&self) -> LintConfig { + LintConfig::help().with_enabled(false) + } + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner 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: &SqfLintData, + ) -> hemtt_workspace::reporting::Codes { + if target.issues().is_empty() { + return Vec::new(); + }; + let Some(processed) = processed else { + return Vec::new(); + }; + + let check_params = if let Some(toml::Value::Boolean(b)) = config.option("check_params") { + *b + } else { + false + }; + let mut errors: Codes = Vec::new(); + for issue in target.issues() { + if let Issue::Unused(var, source) = issue { + match source { + VarSource::Assignment(_) => {} + VarSource::Params(_) => { + if !check_params { + continue; + }; + } + _ => { + continue; + } + } + errors.push(Arc::new(CodeS14Unused::new( + source.get_range().unwrap_or_default(), + var.to_owned(), + None, + config.severity(), + processed, + ))); + } + } + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS14Unused { + span: Range, + token_name: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS14Unused { + fn ident(&self) -> &'static str { + "L-S14" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#unused") + } + /// Top message + fn message(&self) -> String { + format!("Unused Var - {}", self.token_name) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + fn severity(&self) -> Severity { + self.severity + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS14Unused { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + token_name: error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/analyze/lints/s15_shadowed.rs b/libs/sqf/src/analyze/lints/s15_shadowed.rs new file mode 100644 index 000000000..382649540 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s15_shadowed.rs @@ -0,0 +1,136 @@ +use crate::{ + analyze::{inspector::Issue, SqfLintData}, + 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::lint!(LintS15Shadowed); + +impl Lint for LintS15Shadowed { + fn ident(&self) -> &str { + "shadowed" + } + fn sort(&self) -> u32 { + 150 + } + fn description(&self) -> &str { + "Shadowed Var" + } + fn documentation(&self) -> &str { + r"### Example + +**Incorrect** +```sqf +private _z = 5; +private _z = 5; +``` + +### Explanation + +Checks for variables being shadowed." + } + fn default_config(&self) -> LintConfig { + LintConfig::help().with_enabled(false) + } + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner 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: &SqfLintData, + ) -> 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::Shadowed(var, range) = issue { + errors.push(Arc::new(CodeS15Shadowed::new( + range.to_owned(), + var.to_owned(), + None, + config.severity(), + processed, + ))); + } + } + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS15Shadowed { + span: Range, + token_name: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS15Shadowed { + fn ident(&self) -> &'static str { + "L-S15" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#shadowed") + } + /// Top message + fn message(&self) -> String { + format!("Shadowed Var - {}", self.token_name) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + fn severity(&self) -> Severity { + self.severity + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS15Shadowed { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + token_name: error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/analyze/lints/s16_not_private.rs b/libs/sqf/src/analyze/lints/s16_not_private.rs new file mode 100644 index 000000000..a938b28f2 --- /dev/null +++ b/libs/sqf/src/analyze/lints/s16_not_private.rs @@ -0,0 +1,135 @@ +use crate::{ + analyze::{inspector::Issue, SqfLintData}, + 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::lint!(LintS16NotPrivate); + +impl Lint for LintS16NotPrivate { + fn ident(&self) -> &str { + "not_private" + } + fn sort(&self) -> u32 { + 160 + } + fn description(&self) -> &str { + "Not Private Var" + } + fn documentation(&self) -> &str { + r"### Example + +**Incorrect** +```sqf +_z = 6; +``` + +### Explanation + +Checks local variables that are not private." + } + fn default_config(&self) -> LintConfig { + LintConfig::help().with_enabled(false) + } + fn runners(&self) -> Vec>> { + vec![Box::new(Runner)] + } +} + +pub struct Runner; +impl LintRunner 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: &SqfLintData, + ) -> 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::NotPrivate(var, range) = issue { + errors.push(Arc::new(CodeS16NotPrivate::new( + range.to_owned(), + var.to_owned(), + None, + config.severity(), + processed, + ))); + } + } + errors + } +} + +#[allow(clippy::module_name_repetitions)] +pub struct CodeS16NotPrivate { + span: Range, + token_name: String, + error_hint: Option, + severity: Severity, + diagnostic: Option, +} + +impl Code for CodeS16NotPrivate { + fn ident(&self) -> &'static str { + "L-S16" + } + fn link(&self) -> Option<&str> { + Some("/analysis/sqf.html#not_private") + } + /// Top message + fn message(&self) -> String { + format!("Not Private - {}", self.token_name) + } + /// Under ^^^span hint + fn label_message(&self) -> String { + String::new() + } + /// bottom note + fn note(&self) -> Option { + self.error_hint.clone() + } + fn severity(&self) -> Severity { + self.severity + } + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl CodeS16NotPrivate { + #[must_use] + pub fn new( + span: Range, + error_type: String, + error_hint: Option, + severity: Severity, + processed: &Processed, + ) -> Self { + Self { + span, + token_name: error_type, + error_hint, + severity, + diagnostic: None, + } + .generate_processed(processed) + } + fn generate_processed(mut self, processed: &Processed) -> Self { + self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self + } +} diff --git a/libs/sqf/src/lib.rs b/libs/sqf/src/lib.rs index 3174a6ba3..0d0356b2b 100644 --- a/libs/sqf/src/lib.rs +++ b/libs/sqf/src/lib.rs @@ -11,6 +11,7 @@ use std::{ops::Range, sync::Arc}; pub use self::error::Error; +use analyze::inspector::Issue; use arma3_wiki::model::Version; #[doc(no_inline)] pub use float_ord::FloatOrd as Scalar; @@ -23,7 +24,7 @@ pub struct Statements { /// This isn't required to actually be anything significant, but will be displayed in-game if a script error occurs. source: Arc, span: Range, - top_level: bool, + issues: Vec, } impl Statements { @@ -42,6 +43,10 @@ impl Statements { self.span.clone() } + #[must_use] + pub const fn issues(&self) -> &Vec { + &self.issues + } #[must_use] /// Gets the highest version required by any command in this code chunk. pub fn required_version(&self, database: &Database) -> (String, Version, Range) { @@ -105,6 +110,9 @@ impl Statements { } (command, version, span) } + pub fn testing_clear_issues(&mut self) { + self.issues.clear(); + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/libs/sqf/src/parser/mod.rs b/libs/sqf/src/parser/mod.rs index 6634352b1..d69d3cd66 100644 --- a/libs/sqf/src/parser/mod.rs +++ b/libs/sqf/src/parser/mod.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use self::database::{is_special_command, Database}; use self::lexer::{Control, Operator, Token}; +use crate::analyze::inspector; use crate::{BinaryCommand, Expression, NularCommand, Statement, Statements, UnaryCommand}; use chumsky::prelude::*; @@ -43,7 +44,7 @@ pub fn run(database: &Database, processed: &Processed) -> Result( source: processed.extract(span.clone()), span, content, - top_level: false, + issues: vec![], }) }) } diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs index 5771aa632..2441b17ca 100644 --- a/libs/sqf/tests/inspector.rs +++ b/libs/sqf/tests/inspector.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use hemtt_sqf::Statements; use hemtt_workspace::reporting::Processed; @@ -9,7 +7,7 @@ use hemtt_sqf::parser::database::Database; use hemtt_workspace::LayerType; const ROOT: &str = "tests/inspector/"; -fn get_statements(file: &str) -> (Processed, Statements, Arc) { +fn get_statements(file: &str) -> (Processed, Statements, Database) { let folder = std::path::PathBuf::from(ROOT); let workspace = hemtt_workspace::Workspace::builder() .physical(&folder, LayerType::Source) @@ -18,27 +16,28 @@ fn get_statements(file: &str) -> (Processed, Statements, Arc) { let source = workspace.join(file).expect("for test"); let processed = Processor::run(&source).expect("for test"); let statements = hemtt_sqf::parser::run(&Database::a3(false), &processed).expect("for test"); - let database = Arc::new(Database::a3(false)); + let database = Database::a3(false); (processed, statements, database) } #[cfg(test)] mod tests { use crate::get_statements; - use hemtt_sqf::analyze::inspector::{self, Issue, VarSource}; + use hemtt_sqf::analyze::inspector::{Issue, VarSource}; #[test] pub fn test_0() { - let (pro, sqf, database) = get_statements("test_0.sqf"); - let result = inspector::run_processed(&sqf, &pro, &database, true); + let (_pro, sqf, _database) = get_statements("test_0.sqf"); + // let result = inspector::run_processed(&sqf, &pro, &database); + let result = sqf.issues(); println!("done: {}, {result:?}", result.len()); } #[test] pub fn test_1() { - let (pro, sqf, database) = get_statements("test_1.sqf"); - let result = inspector::run_processed(&sqf, &pro, &database, true); - assert_eq!(result.len(), 11); + let (_pro, sqf, _database) = get_statements("test_1.sqf"); + let result = sqf.issues(); + assert_eq!(result.len(), 12); // Order not guarenteed assert!(result.iter().any(|i| { if let Issue::InvalidArgs(cmd, _) = i { @@ -117,12 +116,19 @@ mod tests { false } })); + assert!(result.iter().any(|i| { + if let Issue::InvalidArgs(cmd, _) = i { + cmd == "[B:drawIcon]" + } else { + false + } + })); } #[test] pub fn test_2() { - let (pro, sqf, database) = get_statements("test_2.sqf"); - let result = inspector::run_processed(&sqf, &pro, &database, true); + let (_pro, sqf, _database) = get_statements("test_2.sqf"); + let result = sqf.issues(); assert_eq!(result.len(), 0); } } diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index 2d68148c5..9a5ad9342 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -1,28 +1,31 @@ +// _var[LETTER] are safe +// _test[NUMBER] are errors + a setFuel b; a setFuel 0; a setFuel true; // invalidArgs: takes number 0-1 _test2 setDamage 1; // undefiend -private _var1 = player; -_var1 setDamage 0.5; +private _varA = player; +_varA setDamage 0.5; _test3 = 7; // not private systemChat str _test3; -private "_var2"; -_var2 = 8; -private ["_var3"]; -_var3 = _var2 + 1; -private _test4 = _var3; // unused +private "_varB"; +_varB = 8; +private ["_varC"]; +_varC = _varB + 1; +private _test4 = _varC; // unused params ["_test5"]; private _test5 = 10; // shadow (same level) diag_log text str _test5; gx = []; gx addPublicVariableEventHandler {}; // args: takes lhs string -for "_var5" from 1 to 20 step 0.5 do { - systemChat str _var5; +for "_varE" from 1 to 20 step 0.5 do { + systemChat str _varE; }; -for [{private _var6 = 0}, {_var6 < 5}, {_var6 = _var6 + 1}] do { - systemChat str _var6; +for [{private _varF = 0}, {_varF < 5}, {_varF = _varF + 1}] do { + systemChat str _varF; }; //IGNORE_PRIVATE_WARNING["_fromUpper"]; @@ -31,27 +34,27 @@ X = _fromUpper; [] call { private "_weird"; //IGNORE_PRIVATE_WARNING["_weird"] - // No way to know the order is different - for "_var7" from 1 to 5 do { - if (_var7%2 == 0) then { + for "_varG" from 1 to 5 do { + if (_varG%2 == 0) then { truck lock _weird; }; - if (_var7%2 == 1) then { + if (_varG%2 == 1) then { _weird = 0.5; }; }; }; -// IGNORE_PRIVATE_WARNING["somePFEH"] - // otherwise will assume it's nil +// IGNORE_PRIVATE_WARNING["somePFEH"] if (z) then { - somePFEH = nil; + somePFEH = nil; // otherwise will assume it's always nil }; if (y) then { setObjectViewDistance somePFEH; }; somehash getOrDefaultCall ["key", {_test8}, true]; //undefined -private _var8 = objNull; -somehash getOrDefaultCall ["key", {_var8}, true]; +private _varH = objNull; +somehash getOrDefaultCall ["key", {_varH}, true]; // Will have _player and _target private _condition = { [_player, _target] call y }; @@ -63,7 +66,8 @@ private _condition = { [_player, _target] call y }; x ctrlSetText _player; // bad arg type [_target, _player] call z; }, - _condition + _condition, + {systemChat str _player; []} ] call ace_interact_menu_fnc_createAction; private _hash = [] call CBA_fnc_hashCreate; @@ -79,14 +83,34 @@ _test9= _test9 + 1; systemChat str _test9; // invalid }, 0, []] call CBA_fnc_addPerFrameHandler; -private _var9 = 55; -[{systemChat str _var9}] call CBA_fnc_directCall; +private _varI = 55; +[{systemChat str _varI}] call CBA_fnc_directCall; // Will have _x filter = [orig, {_x + 1}] call CBA_fnc_filter; -private _var10 = 123; -[player, {x = _var10}] call ace_common_fnc_cachedcall; +private _varJ = 123; +[player, {x = _varJ}] call ace_common_fnc_cachedcall; for "_test10" from 1 to 1 step 0.1 do {}; [5] params ["_test11"]; + +params [["_varK", objNull, [objNull]]]; +{ + private _varName = vehicleVarName _varK; + _varK setVehicleVarName (_varName + "ok"); +} call CBA_fnc_directCall; + +_this select 0 drawIcon [ // invalidArgs + "#(rgb,1,1,1)color(1,1,1,1)", + [0,1,0,1], + player, + 0, + 0, + 0, + 5555 // text - optional +]; + +private _varL = nil; +call ([{_varL = 0;}, {_varL = 1;}] select (x == 1)); +["A", "B"] select _varL; diff --git a/libs/sqf/tests/lints.rs b/libs/sqf/tests/lints.rs index 2547454ab..9fe928714 100644 --- a/libs/sqf/tests/lints.rs +++ b/libs/sqf/tests/lints.rs @@ -10,17 +10,17 @@ use hemtt_workspace::{addons::Addon, reporting::WorkspaceFiles, LayerType}; const ROOT: &str = "tests/lints/"; macro_rules! analyze { - ($dir:ident) => { + ($dir:ident, $ignore:expr) => { paste::paste! { #[test] fn []() { - test_analyze(stringify!($dir)); + test_analyze(stringify!($dir), $ignore); } } }; } -fn test_analyze(dir: &str) { +fn test_analyze(dir: &str, ignore_inspector: bool) { let folder = std::path::PathBuf::from(ROOT).join(dir); let workspace = hemtt_workspace::Workspace::builder() .physical(&folder, LayerType::Source) @@ -35,7 +35,10 @@ fn test_analyze(dir: &str) { let config = ProjectConfig::from_file(&config_path_full).unwrap(); match hemtt_sqf::parser::run(&database, &processed) { - Ok(sqf) => { + Ok(mut sqf) => { + if ignore_inspector { + sqf.testing_clear_issues(); + } let codes = analyze( &sqf, Some(&config), @@ -73,12 +76,16 @@ fn test_analyze(dir: &str) { }; } -analyze!(s03_static_typename); -analyze!(s04_command_case); -analyze!(s05_if_assign); -analyze!(s06_find_in_str); -analyze!(s07_select_parse_number); -analyze!(s08_format_args); -analyze!(s09_banned_command); -analyze!(s11_if_not_else); -analyze!(s12_inspector); +analyze!(s03_static_typename, true); +analyze!(s04_command_case, true); +analyze!(s05_if_assign, true); +analyze!(s06_find_in_str, true); +analyze!(s07_select_parse_number, true); +analyze!(s08_format_args, true); +analyze!(s09_banned_command, true); +analyze!(s11_if_not_else, true); +analyze!(s12_invalid_args, false); +analyze!(s13_undefined, false); +analyze!(s14_unused, false); +analyze!(s15_shadowed, false); +analyze!(s16_not_private, false); diff --git a/libs/sqf/tests/lints/project_tests.toml b/libs/sqf/tests/lints/project_tests.toml index e18d39f17..22bc5f9ec 100644 --- a/libs/sqf/tests/lints/project_tests.toml +++ b/libs/sqf/tests/lints/project_tests.toml @@ -6,8 +6,15 @@ options.ignore = [ "addPublicVariableEventHandler", ] -[lints.sqf.inspector] -options.check_shadow = true - [lints.sqf] if_not_else = true +shadowed = true +not_private = true + +[lints.sqf.unused] +enabled = true +options.check_params = false + +[lints.sqf.undefined] +enabled = true +options.check_oprhan_code = true \ No newline at end of file diff --git a/libs/sqf/tests/lints/s12_inspector/source.sqf b/libs/sqf/tests/lints/s12_inspector/source.sqf deleted file mode 100644 index e91864ff9..000000000 --- a/libs/sqf/tests/lints/s12_inspector/source.sqf +++ /dev/null @@ -1,4 +0,0 @@ -x setFuel true; // args: takes number 0-1 - -params ["_var1"]; -private _var1 = 7; diff --git a/libs/sqf/tests/lints/s12_inspector/stdout.ansi b/libs/sqf/tests/lints/s12_inspector/stdout.ansi deleted file mode 100644 index 736c60c9b..000000000 --- a/libs/sqf/tests/lints/s12_inspector/stdout.ansi +++ /dev/null @@ -1,13 +0,0 @@ -help[L-S12]: Inspector - Shadowed: _var1 - ┌─ source.sqf:4:1 - │ -4 │ private _var1 = 7; - │ ^^^^^^^^^^^^^^^^^ - - -help[L-S12]: Inspector - Bad Args: [B:setFuel] - ┌─ source.sqf:1:3 - │ -1 │ x setFuel true; // args: takes number 0-1 - │ ^^^^^^^ - diff --git a/libs/sqf/tests/lints/s12_invalid_args/source.sqf b/libs/sqf/tests/lints/s12_invalid_args/source.sqf new file mode 100644 index 000000000..c36004ae1 --- /dev/null +++ b/libs/sqf/tests/lints/s12_invalid_args/source.sqf @@ -0,0 +1,3 @@ +player setPos [0,1,0]; +alive player; +(vehicle player) setFuel true; // bad args: takes number 0-1 diff --git a/libs/sqf/tests/lints/s12_invalid_args/stdout.ansi b/libs/sqf/tests/lints/s12_invalid_args/stdout.ansi new file mode 100644 index 000000000..860b6bd5c --- /dev/null +++ b/libs/sqf/tests/lints/s12_invalid_args/stdout.ansi @@ -0,0 +1,6 @@ +help[L-S12]: Invalid Args - [B:setFuel] + ┌─ source.sqf:3:18 + │ +3 │ (vehicle player) setFuel true; // bad args: takes number 0-1 + │ ^^^^^^^ + diff --git a/libs/sqf/tests/lints/s13_undefined/source.sqf b/libs/sqf/tests/lints/s13_undefined/source.sqf new file mode 100644 index 000000000..17a4d8520 --- /dev/null +++ b/libs/sqf/tests/lints/s13_undefined/source.sqf @@ -0,0 +1 @@ +x = {systemChat _neverDefined;}; diff --git a/libs/sqf/tests/lints/s13_undefined/stdout.ansi b/libs/sqf/tests/lints/s13_undefined/stdout.ansi new file mode 100644 index 000000000..de3c99906 --- /dev/null +++ b/libs/sqf/tests/lints/s13_undefined/stdout.ansi @@ -0,0 +1,8 @@ +help[L-S13]: Undefined Var - _neverDefined + ┌─ source.sqf:1:17 + │ +1 │ x = {systemChat _neverDefined;}; + │ ^^^^^^^^^^^^^ + │ + = note: From Orphan Code - may not be a problem + diff --git a/libs/sqf/tests/lints/s14_unused/source.sqf b/libs/sqf/tests/lints/s14_unused/source.sqf new file mode 100644 index 000000000..5c8f08213 --- /dev/null +++ b/libs/sqf/tests/lints/s14_unused/source.sqf @@ -0,0 +1,2 @@ +private _z = 5; // and never used +params ["_something"]; \ No newline at end of file diff --git a/libs/sqf/tests/lints/s14_unused/stdout.ansi b/libs/sqf/tests/lints/s14_unused/stdout.ansi new file mode 100644 index 000000000..8891cc018 --- /dev/null +++ b/libs/sqf/tests/lints/s14_unused/stdout.ansi @@ -0,0 +1,6 @@ +help[L-S14]: Unused Var - _z + ┌─ source.sqf:1:1 + │ +1 │ private _z = 5; // and never used + │ ^^^^^^^^^^^^^^ + diff --git a/libs/sqf/tests/lints/s15_shadowed/source.sqf b/libs/sqf/tests/lints/s15_shadowed/source.sqf new file mode 100644 index 000000000..c6af8b2b2 --- /dev/null +++ b/libs/sqf/tests/lints/s15_shadowed/source.sqf @@ -0,0 +1,4 @@ +private _z = 5; +private _z = 5; + +systemChat str _z; // dummy use diff --git a/libs/sqf/tests/lints/s15_shadowed/stdout.ansi b/libs/sqf/tests/lints/s15_shadowed/stdout.ansi new file mode 100644 index 000000000..8734505f7 --- /dev/null +++ b/libs/sqf/tests/lints/s15_shadowed/stdout.ansi @@ -0,0 +1,6 @@ +help[L-S15]: Shadowed Var - _z + ┌─ source.sqf:2:1 + │ +2 │ private _z = 5; + │ ^^^^^^^^^^^^^^ + diff --git a/libs/sqf/tests/lints/s16_not_private/source.sqf b/libs/sqf/tests/lints/s16_not_private/source.sqf new file mode 100644 index 000000000..8b7c5246e --- /dev/null +++ b/libs/sqf/tests/lints/s16_not_private/source.sqf @@ -0,0 +1,3 @@ +_z = 6; + +systemChat str _z; // dummy use diff --git a/libs/sqf/tests/lints/s16_not_private/stdout.ansi b/libs/sqf/tests/lints/s16_not_private/stdout.ansi new file mode 100644 index 000000000..6d7053028 --- /dev/null +++ b/libs/sqf/tests/lints/s16_not_private/stdout.ansi @@ -0,0 +1,6 @@ +help[L-S16]: Not Private - _z + ┌─ source.sqf:1:1 + │ +1 │ _z = 6; + │ ^^^^^^ + From 86db38326bb4de2e650dc6d0a8f4d65d27e9c019 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 9 Oct 2024 00:53:04 -0500 Subject: [PATCH 05/24] _forEachIndex --- libs/sqf/src/analyze/inspector/commands.rs | 8 ++++---- libs/sqf/src/analyze/inspector/mod.rs | 6 +++--- libs/sqf/tests/inspector/test_0.sqf | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index 88a443bd6..a348c1e27 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -177,7 +177,7 @@ impl SciptScope { pub fn cmd_generic_call_magic( &mut self, code_possibilities: &HashSet, - magic: &Vec<&str>, + magic: Vec<(&str, GameValue)>, source: &Range, database: &Database, ) -> HashSet { @@ -192,11 +192,11 @@ impl SciptScope { continue; } self.push(); - for var in magic { + for (var, value) in magic.iter() { self.var_assign( var, true, - HashSet::from([GameValue::Anything]), + HashSet::from([value.clone()]), VarSource::Magic(source.clone()), ); } @@ -363,7 +363,7 @@ impl SciptScope { ) -> HashSet { let mut return_value = cmd_set.clone(); // Check: `array select expression` - let _ = self.cmd_generic_call_magic(rhs, &vec!["_x"], source, database); + let _ = self.cmd_generic_call_magic(rhs, vec![("_x", GameValue::Anything)], source, database); // if lhs is array, and rhs is bool/number then put array into return if lhs.len() == 1 && rhs diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 1387e6828..3df52d812 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -334,14 +334,14 @@ impl SciptScope { "then" => Some(self.cmd_b_then(&lhs_set, &rhs_set, database)), "foreach" | "foreachreversed" => Some(self.cmd_generic_call_magic( &lhs_set, - &vec!["_x", "_y", "_forEachIndex"], + vec![("_x", GameValue::Anything), ("_y", GameValue::Anything), ("_forEachIndex", GameValue::Number(None))], source, database, )), "count" => { let _ = self.cmd_generic_call_magic( &lhs_set, - &vec!["_x"], + vec![("_x", GameValue::Anything)], source, database, ); @@ -350,7 +350,7 @@ impl SciptScope { "findif" | "apply" => { let _ = self.cmd_generic_call_magic( &rhs_set, - &vec!["_x"], + vec![("_x", GameValue::Anything)], source, database, ); diff --git a/libs/sqf/tests/inspector/test_0.sqf b/libs/sqf/tests/inspector/test_0.sqf index e69de29bb..6aa0f015e 100644 --- a/libs/sqf/tests/inspector/test_0.sqf +++ b/libs/sqf/tests/inspector/test_0.sqf @@ -0,0 +1,3 @@ +{ + _forEachIndex + "A"; +} forEach z; \ No newline at end of file From 86fd860240d7a33b07c5f748ce497bf58e598f0e Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Mon, 14 Oct 2024 20:58:56 -0500 Subject: [PATCH 06/24] use `params` for possible types --- libs/sqf/src/analyze/inspector/commands.rs | 52 +++++++++++++++------- libs/sqf/src/analyze/inspector/mod.rs | 10 +++-- libs/sqf/tests/inspector.rs | 9 +++- libs/sqf/tests/inspector/test_0.sqf | 3 -- libs/sqf/tests/inspector/test_1.sqf | 11 +++++ libs/sqf/tests/lints.rs | 2 +- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index a348c1e27..00208f1e2 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -69,25 +69,42 @@ impl SciptScope { VarSource::Params(source.clone()), ); } - GameValue::Array(Some(gv_array)) => { - if gv_array.is_empty() { + GameValue::Array(Some(arg_array)) => { + if arg_array.is_empty() || arg_array[0].is_empty() { continue; } - for element in &gv_array[0] { - if let GameValue::String(Some(Expression::String(var, source, _))) = - element - { - if var.is_empty() { - continue; + let GameValue::String(Some(Expression::String(var_name, source, _))) = + &arg_array[0][0] + else { + continue; + }; + if var_name.is_empty() { + continue; + } + let mut var_types = HashSet::new(); + if arg_array.len() > 2 { + for type_p in &arg_array[2] { + if let GameValue::Array(Some(type_array)) = type_p { + for type_i in type_array { + var_types.extend(type_i.iter().cloned()); + } } - self.var_assign( - var.as_ref(), - true, - HashSet::from([GameValue::Anything]), - VarSource::Params(source.clone()), - ); } } + if var_types.is_empty() { + var_types.insert(GameValue::Anything); + } + // Add the default value to types + // It should be possible to move this above the is_empty check but not always safe + if arg_array.len() > 1 && !arg_array[1].is_empty() { + var_types.insert(arg_array[1][0].clone()); + } + self.var_assign( + var_name.as_ref(), + true, + var_types, + VarSource::Params(source.clone()), + ); } _ => {} } @@ -177,7 +194,7 @@ impl SciptScope { pub fn cmd_generic_call_magic( &mut self, code_possibilities: &HashSet, - magic: Vec<(&str, GameValue)>, + magic: &Vec<(&str, GameValue)>, source: &Range, database: &Database, ) -> HashSet { @@ -192,7 +209,7 @@ impl SciptScope { continue; } self.push(); - for (var, value) in magic.iter() { + for (var, value) in magic { self.var_assign( var, true, @@ -363,7 +380,8 @@ impl SciptScope { ) -> HashSet { let mut return_value = cmd_set.clone(); // Check: `array select expression` - let _ = self.cmd_generic_call_magic(rhs, vec![("_x", GameValue::Anything)], source, database); + let _ = + self.cmd_generic_call_magic(rhs, &vec![("_x", GameValue::Anything)], source, database); // if lhs is array, and rhs is bool/number then put array into return if lhs.len() == 1 && rhs diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 3df52d812..bd113503e 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -334,14 +334,18 @@ impl SciptScope { "then" => Some(self.cmd_b_then(&lhs_set, &rhs_set, database)), "foreach" | "foreachreversed" => Some(self.cmd_generic_call_magic( &lhs_set, - vec![("_x", GameValue::Anything), ("_y", GameValue::Anything), ("_forEachIndex", GameValue::Number(None))], + &vec![ + ("_x", GameValue::Anything), + ("_y", GameValue::Anything), + ("_forEachIndex", GameValue::Number(None)), + ], source, database, )), "count" => { let _ = self.cmd_generic_call_magic( &lhs_set, - vec![("_x", GameValue::Anything)], + &vec![("_x", GameValue::Anything)], source, database, ); @@ -350,7 +354,7 @@ impl SciptScope { "findif" | "apply" => { let _ = self.cmd_generic_call_magic( &rhs_set, - vec![("_x", GameValue::Anything)], + &vec![("_x", GameValue::Anything)], source, database, ); diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs index 2441b17ca..bc92976b6 100644 --- a/libs/sqf/tests/inspector.rs +++ b/libs/sqf/tests/inspector.rs @@ -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(), 12); + assert_eq!(result.len(), 13); // Order not guarenteed assert!(result.iter().any(|i| { if let Issue::InvalidArgs(cmd, _) = i { @@ -123,6 +123,13 @@ mod tests { false } })); + assert!(result.iter().any(|i| { + if let Issue::InvalidArgs(cmd, _) = i { + cmd == "[B:setGusts]" + } else { + false + } + })); } #[test] diff --git a/libs/sqf/tests/inspector/test_0.sqf b/libs/sqf/tests/inspector/test_0.sqf index 6aa0f015e..e69de29bb 100644 --- a/libs/sqf/tests/inspector/test_0.sqf +++ b/libs/sqf/tests/inspector/test_0.sqf @@ -1,3 +0,0 @@ -{ - _forEachIndex + "A"; -} forEach z; \ No newline at end of file diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index 9a5ad9342..4f7ad86a9 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -114,3 +114,14 @@ _this select 0 drawIcon [ // invalidArgs private _varL = nil; call ([{_varL = 0;}, {_varL = 1;}] select (x == 1)); ["A", "B"] select _varL; + +params xParams; +params []; +params [[""]]; +params [["_varM", "", [""]], ["_varN", 1, [0]], ["_varO", { systemChat _varM }]]; +_varM = _varM + ""; +_varN = _varN + 2; +call _varO; + +params [["_someString", "abc", [""]], ["_someCode", { 60 setGusts _someString }]]; +call _someCode; // InvalidArgs for setGusts diff --git a/libs/sqf/tests/lints.rs b/libs/sqf/tests/lints.rs index 257ce4401..992e48212 100644 --- a/libs/sqf/tests/lints.rs +++ b/libs/sqf/tests/lints.rs @@ -90,4 +90,4 @@ analyze!(s14_unused, false); analyze!(s15_shadowed, false); analyze!(s16_not_private, false); -analyze!(s18_in_vehicle_check, false); \ No newline at end of file +analyze!(s18_in_vehicle_check, true); From 1878680546d3c36b614c8b6ad68a148fccd036b1 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 20 Oct 2024 18:41:42 -0500 Subject: [PATCH 07/24] update snaps? --- libs/sqf/tests/optimizer.rs | 6 +++--- .../tests/snapshots/lints__simple_s06_find_in_str.snap | 4 ++-- .../tests/snapshots/optimizer__simple_consume_array.snap | 3 +++ libs/sqf/tests/snapshots/optimizer__simple_scalar.snap | 1 + .../tests/snapshots/optimizer__simple_static_math.snap | 1 + .../tests/snapshots/optimizer__simple_string_case.snap | 1 + .../tests/snapshots/simple__simple_eventhandler-2.snap | 8 ++++++++ libs/sqf/tests/snapshots/simple__simple_foreach-2.snap | 3 +++ .../tests/snapshots/simple__simple_get_visibility-2.snap | 2 ++ libs/sqf/tests/snapshots/simple__simple_include-2.snap | 1 + libs/sqf/tests/snapshots/simple__simple_semicolons-2.snap | 1 + 11 files changed, 26 insertions(+), 5 deletions(-) diff --git a/libs/sqf/tests/optimizer.rs b/libs/sqf/tests/optimizer.rs index 953a5c82b..8222e88c5 100644 --- a/libs/sqf/tests/optimizer.rs +++ b/libs/sqf/tests/optimizer.rs @@ -31,7 +31,7 @@ fn optimize(file: &str) -> Statements { .unwrap(); let source = workspace.join(format!("{file}.sqf")).unwrap(); let processed = Processor::run(&source).unwrap(); - hemtt_sqf::parser::run(&Database::a3(false), &processed) - .unwrap() - .optimize() + let mut sqf = hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap(); + sqf.testing_clear_issues(); + sqf.optimize() } diff --git a/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap b/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap index 69e71fcf0..9d73e9754 100644 --- a/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap +++ b/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap @@ -1,6 +1,6 @@ --- source: libs/sqf/tests/lints.rs -expression: lint(stringify! (s06_find_in_str)) +expression: "lint(stringify! (s06_find_in_str), true)" --- help[L-S06]: string search using `in` is faster than `find` ┌─ s06_find_in_str.sqf:1:1 @@ -17,4 +17,4 @@ expression: lint(stringify! (s06_find_in_str)) 2 │ private _hasBar = things find "bar" > -1; │ ^^^^^^^^^^^^^^^^^^^^^^ using `find` with -1 │ - = try: "bar" in _things + = try: "bar" in things diff --git a/libs/sqf/tests/snapshots/optimizer__simple_consume_array.snap b/libs/sqf/tests/snapshots/optimizer__simple_consume_array.snap index 3ea53afbb..41eb91d63 100644 --- a/libs/sqf/tests/snapshots/optimizer__simple_consume_array.snap +++ b/libs/sqf/tests/snapshots/optimizer__simple_consume_array.snap @@ -300,6 +300,7 @@ Statements { ], source: "1;2;3;4;", span: 247..255, + issues: [], }, ), Code( @@ -326,6 +327,7 @@ Statements { ], source: "-1;-2;", span: 265..271, + issues: [], }, ), ], @@ -380,4 +382,5 @@ Statements { ], source: "params [\"_a\", \"_b\"];\n\nparams [\"_a\", \"_b\", [\"_c\", []]];\n\nmissionNamespace getVariable [\"a\", -1];\n\nz setVariable [\"b\", [], true];\n\n[1,0] vectorAdd p;\n\npositionCameraToWorld [10000, 0, 10000];\n\n\nrandom [0, _x, 1];\n\nprivate _z = if (time > 10) then { 1;2;3;4; } else { -1;-2; };\n\nparam [\"_d\"];\n\n[] param [\"_e\"];\n", span: 0..307, + issues: [], } diff --git a/libs/sqf/tests/snapshots/optimizer__simple_scalar.snap b/libs/sqf/tests/snapshots/optimizer__simple_scalar.snap index a1551d853..01cb27800 100644 --- a/libs/sqf/tests/snapshots/optimizer__simple_scalar.snap +++ b/libs/sqf/tests/snapshots/optimizer__simple_scalar.snap @@ -16,4 +16,5 @@ Statements { ], source: "-5;\n", span: 0..3, + issues: [], } diff --git a/libs/sqf/tests/snapshots/optimizer__simple_static_math.snap b/libs/sqf/tests/snapshots/optimizer__simple_static_math.snap index 0110c8076..bd12765d5 100644 --- a/libs/sqf/tests/snapshots/optimizer__simple_static_math.snap +++ b/libs/sqf/tests/snapshots/optimizer__simple_static_math.snap @@ -46,4 +46,5 @@ Statements { ], source: "1 + (2 * 2) + (36 % 31) + (36 / 6) + (sqrt 100) - 3;\n\nsqrt -100;\n\n\nz + z;\n", span: 0..73, + issues: [], } diff --git a/libs/sqf/tests/snapshots/optimizer__simple_string_case.snap b/libs/sqf/tests/snapshots/optimizer__simple_string_case.snap index e81e85642..3e6cabd8c 100644 --- a/libs/sqf/tests/snapshots/optimizer__simple_string_case.snap +++ b/libs/sqf/tests/snapshots/optimizer__simple_string_case.snap @@ -15,4 +15,5 @@ Statements { ], source: "toLower \"A\" + toUpper \"b\" + toUpperAnsi \"C\" + toLowerAnsi \"d\";\n", span: 0..62, + issues: [], } diff --git a/libs/sqf/tests/snapshots/simple__simple_eventhandler-2.snap b/libs/sqf/tests/snapshots/simple__simple_eventhandler-2.snap index e09ece6fb..09537998d 100644 --- a/libs/sqf/tests/snapshots/simple__simple_eventhandler-2.snap +++ b/libs/sqf/tests/snapshots/simple__simple_eventhandler-2.snap @@ -27,6 +27,7 @@ expression: ast ], source: "deleteVehicle _x", span: 3..19, + issues: [], }, ), NularCommand( @@ -110,6 +111,7 @@ expression: ast ], source: "alive _x", span: 115..123, + issues: [], }, ), 106..112, @@ -140,6 +142,7 @@ expression: ast ], source: "deleteVehicle _x;", span: 149..166, + issues: [], }, ), NularCommand( @@ -155,6 +158,7 @@ expression: ast ], source: "allPlayers findIf { alive _x };\n {\n deleteVehicle _x;\n } forEach allPlayers;", span: 95..196, + issues: [], }, ), 80..84, @@ -164,6 +168,7 @@ expression: ast ], source: "if (alive player) then {\n allPlayers findIf { alive _x };\n {\n deleteVehicle _x;\n } forEach allPlayers;\n };", span: 62..203, + issues: [], }, ), ], @@ -242,6 +247,7 @@ expression: ast ], source: "deleteVehicle _x", span: 294..310, + issues: [], }, ), NularCommand( @@ -257,6 +263,7 @@ expression: ast ], source: "{ deleteVehicle _x } count allPlayers;", span: 292..330, + issues: [], }, ), 277..281, @@ -266,6 +273,7 @@ expression: ast ], source: "if (alive player) then {\n { deleteVehicle _x } count allPlayers;\n };", span: 259..337, + issues: [], }, ), ], diff --git a/libs/sqf/tests/snapshots/simple__simple_foreach-2.snap b/libs/sqf/tests/snapshots/simple__simple_foreach-2.snap index 24bc3e967..96cab0241 100644 --- a/libs/sqf/tests/snapshots/simple__simple_foreach-2.snap +++ b/libs/sqf/tests/snapshots/simple__simple_foreach-2.snap @@ -27,6 +27,7 @@ expression: ast ], source: "deleteVehicle _x;", span: 7..24, + issues: [], }, ), NularCommand( @@ -106,6 +107,7 @@ expression: ast ], source: "_x setDamage 1;", span: 97..112, + issues: [], }, ), UnaryCommand( @@ -125,6 +127,7 @@ expression: ast ], source: "systemChat format [\"%1\", _x];\n {\n _x setDamage 1;\n } forEach crew _x;", span: 53..135, + issues: [], }, ), NularCommand( diff --git a/libs/sqf/tests/snapshots/simple__simple_get_visibility-2.snap b/libs/sqf/tests/snapshots/simple__simple_get_visibility-2.snap index 0e168ddfe..3c935a5e2 100644 --- a/libs/sqf/tests/snapshots/simple__simple_get_visibility-2.snap +++ b/libs/sqf/tests/snapshots/simple__simple_get_visibility-2.snap @@ -86,6 +86,7 @@ expression: ast ], source: "_arg1 = [eyePos _arg1, _arg1]", span: 66..95, + issues: [], }, ), 59..63, @@ -151,6 +152,7 @@ expression: ast ], source: "_arg2 = [eyePos _arg2, _arg2]", span: 138..167, + issues: [], }, ), 131..135, diff --git a/libs/sqf/tests/snapshots/simple__simple_include-2.snap b/libs/sqf/tests/snapshots/simple__simple_include-2.snap index 726ce909b..1adfd6f81 100644 --- a/libs/sqf/tests/snapshots/simple__simple_include-2.snap +++ b/libs/sqf/tests/snapshots/simple__simple_include-2.snap @@ -130,6 +130,7 @@ expression: ast ], source: "private thinghi = _x + \"test\";", span: 94..124, + issues: [], }, ), Array( diff --git a/libs/sqf/tests/snapshots/simple__simple_semicolons-2.snap b/libs/sqf/tests/snapshots/simple__simple_semicolons-2.snap index ede822834..a982af3d4 100644 --- a/libs/sqf/tests/snapshots/simple__simple_semicolons-2.snap +++ b/libs/sqf/tests/snapshots/simple__simple_semicolons-2.snap @@ -78,6 +78,7 @@ expression: ast ], source: "systemChat \"this is a test\";", span: 153..181, + issues: [], }, ), 136..140, From 8161ba6b25edd8304b955e77b949bdf8c3d0db20 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 20 Oct 2024 21:31:56 -0500 Subject: [PATCH 08/24] update tests --- libs/sqf/tests/lints.rs | 10 +++++----- libs/sqf/tests/lints/project_tests.toml | 2 +- .../source.sqf => s12_invalid_args.sqf} | 0 .../{s13_undefined/source.sqf => s13_undefined.sqf} | 0 .../lints/{s14_unused/source.sqf => s14_unused.sqf} | 0 .../{s15_shadowed/source.sqf => s15_shadowed.sqf} | 0 .../source.sqf => s16_not_private.sqf} | 0 .../lints__simple_s12_invalid_args.snap} | 7 +++++-- .../lints__simple_s13_undefined.snap} | 7 +++++-- .../lints__simple_s14_unused.snap} | 7 +++++-- .../lints__simple_s15_shadowed.snap} | 7 +++++-- .../lints__simple_s16_not_private.snap} | 7 +++++-- 12 files changed, 31 insertions(+), 16 deletions(-) rename libs/sqf/tests/lints/{s12_invalid_args/source.sqf => s12_invalid_args.sqf} (100%) rename libs/sqf/tests/lints/{s13_undefined/source.sqf => s13_undefined.sqf} (100%) rename libs/sqf/tests/lints/{s14_unused/source.sqf => s14_unused.sqf} (100%) rename libs/sqf/tests/lints/{s15_shadowed/source.sqf => s15_shadowed.sqf} (100%) rename libs/sqf/tests/lints/{s16_not_private/source.sqf => s16_not_private.sqf} (100%) rename libs/sqf/tests/{lints/s12_invalid_args/stdout.ansi => snapshots/lints__simple_s12_invalid_args.snap} (63%) rename libs/sqf/tests/{lints/s13_undefined/stdout.ansi => snapshots/lints__simple_s13_undefined.snap} (69%) rename libs/sqf/tests/{lints/s14_unused/stdout.ansi => snapshots/lints__simple_s14_unused.snap} (60%) rename libs/sqf/tests/{lints/s15_shadowed/stdout.ansi => snapshots/lints__simple_s15_shadowed.snap} (58%) rename libs/sqf/tests/{lints/s16_not_private/stdout.ansi => snapshots/lints__simple_s16_not_private.snap} (54%) diff --git a/libs/sqf/tests/lints.rs b/libs/sqf/tests/lints.rs index 4abb13e26..e2b7653c3 100644 --- a/libs/sqf/tests/lints.rs +++ b/libs/sqf/tests/lints.rs @@ -29,11 +29,11 @@ lint!(s07_select_parse_number, true); lint!(s08_format_args, true); lint!(s09_banned_command, true); lint!(s11_if_not_else, true); -// analyze!(s12_invalid_args, false); -// analyze!(s13_undefined, false); -// analyze!(s14_unused, false); -// analyze!(s15_shadowed, false); -// analyze!(s16_not_private, false); +lint!(s12_invalid_args, false); +lint!(s13_undefined, false); +lint!(s14_unused, false); +lint!(s15_shadowed, false); +lint!(s16_not_private, false); lint!(s17_var_all_caps, true); lint!(s18_in_vehicle_check, true); lint!(s20_bool_static_comparison, true); diff --git a/libs/sqf/tests/lints/project_tests.toml b/libs/sqf/tests/lints/project_tests.toml index 6090ebd38..5cde1a131 100644 --- a/libs/sqf/tests/lints/project_tests.toml +++ b/libs/sqf/tests/lints/project_tests.toml @@ -14,7 +14,7 @@ if_not_else = true enabled = true [lints.sqf.not_private] -not_private = true +enabled = true [lints.sqf.unused] enabled = true diff --git a/libs/sqf/tests/lints/s12_invalid_args/source.sqf b/libs/sqf/tests/lints/s12_invalid_args.sqf similarity index 100% rename from libs/sqf/tests/lints/s12_invalid_args/source.sqf rename to libs/sqf/tests/lints/s12_invalid_args.sqf diff --git a/libs/sqf/tests/lints/s13_undefined/source.sqf b/libs/sqf/tests/lints/s13_undefined.sqf similarity index 100% rename from libs/sqf/tests/lints/s13_undefined/source.sqf rename to libs/sqf/tests/lints/s13_undefined.sqf diff --git a/libs/sqf/tests/lints/s14_unused/source.sqf b/libs/sqf/tests/lints/s14_unused.sqf similarity index 100% rename from libs/sqf/tests/lints/s14_unused/source.sqf rename to libs/sqf/tests/lints/s14_unused.sqf diff --git a/libs/sqf/tests/lints/s15_shadowed/source.sqf b/libs/sqf/tests/lints/s15_shadowed.sqf similarity index 100% rename from libs/sqf/tests/lints/s15_shadowed/source.sqf rename to libs/sqf/tests/lints/s15_shadowed.sqf diff --git a/libs/sqf/tests/lints/s16_not_private/source.sqf b/libs/sqf/tests/lints/s16_not_private.sqf similarity index 100% rename from libs/sqf/tests/lints/s16_not_private/source.sqf rename to libs/sqf/tests/lints/s16_not_private.sqf diff --git a/libs/sqf/tests/lints/s12_invalid_args/stdout.ansi b/libs/sqf/tests/snapshots/lints__simple_s12_invalid_args.snap similarity index 63% rename from libs/sqf/tests/lints/s12_invalid_args/stdout.ansi rename to libs/sqf/tests/snapshots/lints__simple_s12_invalid_args.snap index 860b6bd5c..6babe1b4f 100644 --- a/libs/sqf/tests/lints/s12_invalid_args/stdout.ansi +++ b/libs/sqf/tests/snapshots/lints__simple_s12_invalid_args.snap @@ -1,6 +1,9 @@ +--- +source: libs/sqf/tests/lints.rs +expression: "lint(stringify! (s12_invalid_args), false)" +--- help[L-S12]: Invalid Args - [B:setFuel] - ┌─ source.sqf:3:18 + ┌─ s12_invalid_args.sqf:3:18 │ 3 │ (vehicle player) setFuel true; // bad args: takes number 0-1 │ ^^^^^^^ - diff --git a/libs/sqf/tests/lints/s13_undefined/stdout.ansi b/libs/sqf/tests/snapshots/lints__simple_s13_undefined.snap similarity index 69% rename from libs/sqf/tests/lints/s13_undefined/stdout.ansi rename to libs/sqf/tests/snapshots/lints__simple_s13_undefined.snap index de3c99906..71fe54bc8 100644 --- a/libs/sqf/tests/lints/s13_undefined/stdout.ansi +++ b/libs/sqf/tests/snapshots/lints__simple_s13_undefined.snap @@ -1,8 +1,11 @@ +--- +source: libs/sqf/tests/lints.rs +expression: "lint(stringify! (s13_undefined), false)" +--- help[L-S13]: Undefined Var - _neverDefined - ┌─ source.sqf:1:17 + ┌─ s13_undefined.sqf:1:17 │ 1 │ x = {systemChat _neverDefined;}; │ ^^^^^^^^^^^^^ │ = note: From Orphan Code - may not be a problem - diff --git a/libs/sqf/tests/lints/s14_unused/stdout.ansi b/libs/sqf/tests/snapshots/lints__simple_s14_unused.snap similarity index 60% rename from libs/sqf/tests/lints/s14_unused/stdout.ansi rename to libs/sqf/tests/snapshots/lints__simple_s14_unused.snap index 8891cc018..c68fafc5b 100644 --- a/libs/sqf/tests/lints/s14_unused/stdout.ansi +++ b/libs/sqf/tests/snapshots/lints__simple_s14_unused.snap @@ -1,6 +1,9 @@ +--- +source: libs/sqf/tests/lints.rs +expression: "lint(stringify! (s14_unused), false)" +--- help[L-S14]: Unused Var - _z - ┌─ source.sqf:1:1 + ┌─ s14_unused.sqf:1:1 │ 1 │ private _z = 5; // and never used │ ^^^^^^^^^^^^^^ - diff --git a/libs/sqf/tests/lints/s15_shadowed/stdout.ansi b/libs/sqf/tests/snapshots/lints__simple_s15_shadowed.snap similarity index 58% rename from libs/sqf/tests/lints/s15_shadowed/stdout.ansi rename to libs/sqf/tests/snapshots/lints__simple_s15_shadowed.snap index 8734505f7..ae970ed38 100644 --- a/libs/sqf/tests/lints/s15_shadowed/stdout.ansi +++ b/libs/sqf/tests/snapshots/lints__simple_s15_shadowed.snap @@ -1,6 +1,9 @@ +--- +source: libs/sqf/tests/lints.rs +expression: "lint(stringify! (s15_shadowed), false)" +--- help[L-S15]: Shadowed Var - _z - ┌─ source.sqf:2:1 + ┌─ s15_shadowed.sqf:2:1 │ 2 │ private _z = 5; │ ^^^^^^^^^^^^^^ - diff --git a/libs/sqf/tests/lints/s16_not_private/stdout.ansi b/libs/sqf/tests/snapshots/lints__simple_s16_not_private.snap similarity index 54% rename from libs/sqf/tests/lints/s16_not_private/stdout.ansi rename to libs/sqf/tests/snapshots/lints__simple_s16_not_private.snap index 6d7053028..47dd8cd6b 100644 --- a/libs/sqf/tests/lints/s16_not_private/stdout.ansi +++ b/libs/sqf/tests/snapshots/lints__simple_s16_not_private.snap @@ -1,6 +1,9 @@ +--- +source: libs/sqf/tests/lints.rs +expression: "lint(stringify! (s16_not_private), false)" +--- help[L-S16]: Not Private - _z - ┌─ source.sqf:1:1 + ┌─ s16_not_private.sqf:1:1 │ 1 │ _z = 6; │ ^^^^^^ - From 20090060e4cfaf776f02702e7a197e2f4c21eed2 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Mon, 21 Oct 2024 23:50:19 -0500 Subject: [PATCH 09/24] fix config spelling --- libs/sqf/src/analyze/lints/s13_undefined.rs | 6 +++--- libs/sqf/tests/lints/project_tests.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/sqf/src/analyze/lints/s13_undefined.rs b/libs/sqf/src/analyze/lints/s13_undefined.rs index 01ac0a122..6afc46eb8 100644 --- a/libs/sqf/src/analyze/lints/s13_undefined.rs +++ b/libs/sqf/src/analyze/lints/s13_undefined.rs @@ -58,8 +58,8 @@ impl LintRunner for Runner { let Some(processed) = processed else { return Vec::new(); }; - let check_oprhan_code = - if let Some(toml::Value::Boolean(b)) = config.option("check_oprhan_code") { + let check_orphan_code = + if let Some(toml::Value::Boolean(b)) = config.option("check_orphan_code") { *b } else { false @@ -68,7 +68,7 @@ impl LintRunner for Runner { for issue in target.issues() { if let Issue::Undefined(var, range, is_orphan_scope) = issue { let error_hint = if *is_orphan_scope { - if !check_oprhan_code { + if !check_orphan_code { continue; } Some("From Orphan Code - may not be a problem".to_owned()) diff --git a/libs/sqf/tests/lints/project_tests.toml b/libs/sqf/tests/lints/project_tests.toml index 5cde1a131..2fd943a65 100644 --- a/libs/sqf/tests/lints/project_tests.toml +++ b/libs/sqf/tests/lints/project_tests.toml @@ -22,7 +22,7 @@ options.check_params = false [lints.sqf.undefined] enabled = true -options.check_oprhan_code = true +options.check_orphan_code = true [lints.sqf.var_all_caps] enabled = true From b3bb7e66a0030e542f7edd9e9eb4ab809dc6b390 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Mon, 21 Oct 2024 23:53:59 -0500 Subject: [PATCH 10/24] update lints --- libs/sqf/src/analyze/lints/s12_invalid_args.rs | 4 ++-- libs/sqf/src/analyze/lints/s13_undefined.rs | 4 ++-- libs/sqf/src/analyze/lints/s14_unused.rs | 4 ++-- libs/sqf/src/analyze/lints/s15_shadowed.rs | 4 ++-- libs/sqf/src/analyze/lints/s16_not_private.rs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/sqf/src/analyze/lints/s12_invalid_args.rs b/libs/sqf/src/analyze/lints/s12_invalid_args.rs index a535a18a2..af7dbb8dc 100644 --- a/libs/sqf/src/analyze/lints/s12_invalid_args.rs +++ b/libs/sqf/src/analyze/lints/s12_invalid_args.rs @@ -9,7 +9,7 @@ use hemtt_workspace::{ }; use std::{ops::Range, sync::Arc}; -crate::lint!(LintS12InvalidArgs); +crate::analyze::lint!(LintS12InvalidArgs); impl Lint for LintS12InvalidArgs { fn ident(&self) -> &str { @@ -129,7 +129,7 @@ impl CodeS12InvalidArgs { .generate_processed(processed) } fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); self } } diff --git a/libs/sqf/src/analyze/lints/s13_undefined.rs b/libs/sqf/src/analyze/lints/s13_undefined.rs index 6afc46eb8..657998037 100644 --- a/libs/sqf/src/analyze/lints/s13_undefined.rs +++ b/libs/sqf/src/analyze/lints/s13_undefined.rs @@ -9,7 +9,7 @@ use hemtt_workspace::{ }; use std::{ops::Range, sync::Arc}; -crate::lint!(LintS13Undefined); +crate::analyze::lint!(LintS13Undefined); impl Lint for LintS13Undefined { fn ident(&self) -> &str { @@ -143,7 +143,7 @@ impl CodeS13Undefined { .generate_processed(processed) } fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); self } } diff --git a/libs/sqf/src/analyze/lints/s14_unused.rs b/libs/sqf/src/analyze/lints/s14_unused.rs index a35e3ae7c..852f83e1e 100644 --- a/libs/sqf/src/analyze/lints/s14_unused.rs +++ b/libs/sqf/src/analyze/lints/s14_unused.rs @@ -12,7 +12,7 @@ use hemtt_workspace::{ }; use std::{ops::Range, sync::Arc}; -crate::lint!(LintS14Unused); +crate::analyze::lint!(LintS14Unused); impl Lint for LintS14Unused { fn ident(&self) -> &str { @@ -149,7 +149,7 @@ impl CodeS14Unused { .generate_processed(processed) } fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); self } } diff --git a/libs/sqf/src/analyze/lints/s15_shadowed.rs b/libs/sqf/src/analyze/lints/s15_shadowed.rs index 382649540..9f46615e9 100644 --- a/libs/sqf/src/analyze/lints/s15_shadowed.rs +++ b/libs/sqf/src/analyze/lints/s15_shadowed.rs @@ -9,7 +9,7 @@ use hemtt_workspace::{ }; use std::{ops::Range, sync::Arc}; -crate::lint!(LintS15Shadowed); +crate::analyze::lint!(LintS15Shadowed); impl Lint for LintS15Shadowed { fn ident(&self) -> &str { @@ -130,7 +130,7 @@ impl CodeS15Shadowed { .generate_processed(processed) } fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); self } } diff --git a/libs/sqf/src/analyze/lints/s16_not_private.rs b/libs/sqf/src/analyze/lints/s16_not_private.rs index a938b28f2..fe305e9ea 100644 --- a/libs/sqf/src/analyze/lints/s16_not_private.rs +++ b/libs/sqf/src/analyze/lints/s16_not_private.rs @@ -9,7 +9,7 @@ use hemtt_workspace::{ }; use std::{ops::Range, sync::Arc}; -crate::lint!(LintS16NotPrivate); +crate::analyze::lint!(LintS16NotPrivate); impl Lint for LintS16NotPrivate { fn ident(&self) -> &str { @@ -129,7 +129,7 @@ impl CodeS16NotPrivate { .generate_processed(processed) } fn generate_processed(mut self, processed: &Processed) -> Self { - self.diagnostic = Diagnostic::new_for_processed(&self, self.span.clone(), processed); + self.diagnostic = Diagnostic::from_code_processed(&self, self.span.clone(), processed); self } } From a7d9792e52426b67f727ab6ce628d58512f5f865 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 3 Nov 2024 11:11:40 -0600 Subject: [PATCH 11/24] Update game_value.rs --- libs/sqf/src/analyze/inspector/game_value.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index 080aae781..5a3e29e01 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -126,6 +126,7 @@ impl GameValue { "set3DENMissionAttributes", "setPiPEffect", "ppEffectCreate", + "inAreaArray", ]; if !WIKI_CMDS_IGNORE_MISSING_PARAM.contains(&cmd_name) { warn!("cmd {cmd_name} - param {name} not found"); From 8838a34e07d22deb59439510235edf9306bd78a8 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 10 Nov 2024 15:18:34 -0600 Subject: [PATCH 12/24] use generic version of value for param types --- libs/sqf/src/analyze/inspector/commands.rs | 7 +++-- .../analyze/inspector/external_functions.rs | 2 ++ libs/sqf/src/analyze/inspector/game_value.rs | 29 ++++++++++++------- libs/sqf/src/analyze/inspector/mod.rs | 1 + libs/sqf/tests/inspector/test_1.sqf | 5 ++++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index 00208f1e2..d955339f9 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -1,3 +1,5 @@ +//! Emulates engine commands + use std::{collections::HashSet, ops::Range}; use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; @@ -86,7 +88,7 @@ impl SciptScope { for type_p in &arg_array[2] { if let GameValue::Array(Some(type_array)) = type_p { for type_i in type_array { - var_types.extend(type_i.iter().cloned()); + var_types.extend(type_i.iter().map(GameValue::make_generic)); } } } @@ -95,7 +97,8 @@ impl SciptScope { var_types.insert(GameValue::Anything); } // Add the default value to types - // It should be possible to move this above the is_empty check but not always safe + // It would be nice to move this above the is_empty check but not always safe + // ie: assume `params ["_z", ""]` is type string, but this is not guarentted if arg_array.len() > 1 && !arg_array[1].is_empty() { var_types.insert(arg_array[1][0].clone()); } diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index e7d6f1cbb..a70b7e5a7 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -1,3 +1,5 @@ +//! Emulate how common external functions will handle code + use std::collections::HashSet; use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index 5a3e29e01..aa3fa805d 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -1,3 +1,5 @@ +//! Game Values and mapping them from commands + use std::collections::HashSet; use arma3_wiki::model::{Arg, Call, Param, Value}; @@ -129,7 +131,7 @@ impl GameValue { "inAreaArray", ]; if !WIKI_CMDS_IGNORE_MISSING_PARAM.contains(&cmd_name) { - warn!("cmd {cmd_name} - param {name} not found"); + // warn!("cmd {cmd_name} - param {name} not found"); } return true; }; @@ -146,7 +148,7 @@ impl GameValue { return true; } - let test = set.iter().any(|s| { + set.iter().any(|s| { match s { Self::Anything | Self::Array(None) => { // println!("array (any/generic) pass"); @@ -154,9 +156,7 @@ impl GameValue { } Self::Array(Some(gv_array)) => { // println!("array (gv: {}) expected (arg: {})", gv_array.len(), arg_array.len()); - // if gv_array.len() > arg_array.len() { - // not really an error, some syntaxes take more than others - // } + // note: some syntaxes take more than others for (index, arg) in arg_array.iter().enumerate() { let possible = if index < gv_array.len() { gv_array[index].iter().cloned().collect() @@ -171,9 +171,7 @@ impl GameValue { } _ => false, } - }); - - test + }) } } } @@ -190,7 +188,6 @@ impl GameValue { #[must_use] /// matches values are compatible (Anything will always match) - /// todo: think about how nil and any interact? pub fn match_values(left: &Self, right: &Self) -> bool { if matches!(left, Self::Anything) { return true; @@ -247,7 +244,19 @@ impl GameValue { } } } - + #[must_use] + /// Gets a generic version of a type + pub fn make_generic(&self) -> Self { + match self { + Self::Array(_) => Self::Array(None), + Self::Boolean(_) => Self::Boolean(None), + Self::Code(_) => Self::Code(None), + Self::ForType(_) => Self::ForType(None), + Self::Number(_) => Self::Number(None), + Self::String(_) => Self::String(None), + _ => self.clone() + } + } #[must_use] /// Get as a string for debugging pub fn as_debug(&self) -> String { diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index bd113503e..8add67d65 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -328,6 +328,7 @@ impl SciptScope { } "do" => { // from While, With, For, and Switch + // todo: handle switch return value Some(self.cmd_b_do(&lhs_set, &rhs_set, database)) } "from" | "to" | "step" => Some(self.cmd_b_from_chain(&lhs_set, &rhs_set)), diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index 4f7ad86a9..d83115a90 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -125,3 +125,8 @@ call _varO; params [["_someString", "abc", [""]], ["_someCode", { 60 setGusts _someString }]]; call _someCode; // InvalidArgs for setGusts + +// ensure we use a generic version of the array param types or format would have an error +params [["_varP", "", ["", []]]]; +format _varP; + From b686b2a2f4e9bc601b413e775cbd8ae46807a32d Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 24 Nov 2024 15:13:36 -0600 Subject: [PATCH 13/24] Improve checking code in external scopes --- .../analyze/inspector/external_functions.rs | 18 ++++++++++-- libs/sqf/src/analyze/inspector/mod.rs | 1 + libs/sqf/tests/inspector.rs | 28 ++++++++++++++----- libs/sqf/tests/inspector/test_1.sqf | 8 ++++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index a70b7e5a7..f96d8b8e5 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -89,7 +89,7 @@ impl SciptScope { } } } - "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" => { + "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" | "cba_fnc_execnextframe" => { if !gv_array.is_empty() { self.external_new_scope(&gv_array[0], &vec![], database); } @@ -113,6 +113,20 @@ impl SciptScope { ); } } + "cba_fnc_addeventhandlerargs" => { + if gv_array.len() > 1 { + self.external_new_scope( + &gv_array[1], + &vec![ + ("_thisType", GameValue::String(None)), + ("_thisId", GameValue::Number(None)), + ("_thisFnc", GameValue::Code(None)), + ("_thisArgs", GameValue::Anything), + ], + database, + ); + } + } _ => {} }, _ => {} @@ -135,7 +149,7 @@ impl SciptScope { if self.code_used.contains(expression) { return; } - let mut ext_scope = Self::create(&self.ignored_vars, true); + let mut ext_scope = Self::create(&self.ignored_vars, false); for (var, value) in vars { ext_scope.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 8add67d65..0313a736b 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -70,6 +70,7 @@ pub struct SciptScope { local: Vec, code_seen: HashSet, code_used: HashSet, + /// Orphan scopes are code blocks that are created but don't appear to be called in a known way is_orphan_scope: bool, ignored_vars: HashSet, } diff --git a/libs/sqf/tests/inspector.rs b/libs/sqf/tests/inspector.rs index bc92976b6..2e5c630aa 100644 --- a/libs/sqf/tests/inspector.rs +++ b/libs/sqf/tests/inspector.rs @@ -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(), 13); + assert_eq!(result.len(), 15); // Order not guarenteed assert!(result.iter().any(|i| { if let Issue::InvalidArgs(cmd, _) = i { @@ -47,8 +47,8 @@ mod tests { } })); assert!(result.iter().any(|i| { - if let Issue::Undefined(var, _, _) = i { - var == "_test2" + if let Issue::Undefined(var, _, orphan) = i { + var == "_test2" && !orphan } else { false } @@ -89,15 +89,15 @@ mod tests { } })); assert!(result.iter().any(|i| { - if let Issue::Undefined(var, _, _) = i { - var == "_test8" + if let Issue::Undefined(var, _, orphan) = i { + var == "_test8" && !orphan } else { false } })); assert!(result.iter().any(|i| { - if let Issue::Undefined(var, _, _) = i { - var == "_test9" + if let Issue::Undefined(var, _, orphan) = i { + var == "_test9" && !orphan } else { false } @@ -130,6 +130,20 @@ mod tests { false } })); + assert!(result.iter().any(|i| { + if let Issue::Undefined(var, _, orphan) = i { + var == "_test12" && !orphan + } else { + false + } + })); + assert!(result.iter().any(|i| { + if let Issue::Undefined(var, _, orphan) = i { + var == "_test13" && *orphan + } else { + false + } + })); } #[test] diff --git a/libs/sqf/tests/inspector/test_1.sqf b/libs/sqf/tests/inspector/test_1.sqf index d83115a90..6505d61cd 100644 --- a/libs/sqf/tests/inspector/test_1.sqf +++ b/libs/sqf/tests/inspector/test_1.sqf @@ -130,3 +130,11 @@ call _someCode; // InvalidArgs for setGusts params [["_varP", "", ["", []]]]; format _varP; + +[{ + [_test12] call some_func; // undef, not orphan because CBA_fnc_execNextFrame is a known clean scope +}, player] call CBA_fnc_execNextFrame; +[{ + [_test13] call some_func; // undef, is orphan +}, player] call unknown_fnc_Usage; + From 2fa6d8b631ecd09ecfbf18038b3602f3fbde989d Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sat, 14 Dec 2024 00:02:21 -0600 Subject: [PATCH 14/24] func sig change --- libs/sqf/src/analyze/lints/s12_invalid_args.rs | 6 +++--- libs/sqf/src/analyze/lints/s13_undefined.rs | 6 +++--- libs/sqf/src/analyze/lints/s14_unused.rs | 6 +++--- libs/sqf/src/analyze/lints/s15_shadowed.rs | 6 +++--- libs/sqf/src/analyze/lints/s16_not_private.rs | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/libs/sqf/src/analyze/lints/s12_invalid_args.rs b/libs/sqf/src/analyze/lints/s12_invalid_args.rs index af7dbb8dc..acff86ac3 100644 --- a/libs/sqf/src/analyze/lints/s12_invalid_args.rs +++ b/libs/sqf/src/analyze/lints/s12_invalid_args.rs @@ -12,16 +12,16 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS12InvalidArgs); impl Lint for LintS12InvalidArgs { - fn ident(&self) -> &str { + fn ident(&self) -> &'static str { "invalid_args" } fn sort(&self) -> u32 { 120 } - fn description(&self) -> &str { + fn description(&self) -> &'static str { "Invalid Args" } - fn documentation(&self) -> &str { + fn documentation(&self) -> &'static str { r"### Example **Incorrect** diff --git a/libs/sqf/src/analyze/lints/s13_undefined.rs b/libs/sqf/src/analyze/lints/s13_undefined.rs index 657998037..9ee4691e8 100644 --- a/libs/sqf/src/analyze/lints/s13_undefined.rs +++ b/libs/sqf/src/analyze/lints/s13_undefined.rs @@ -12,16 +12,16 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS13Undefined); impl Lint for LintS13Undefined { - fn ident(&self) -> &str { + fn ident(&self) -> &'static str { "undefined" } fn sort(&self) -> u32 { 130 } - fn description(&self) -> &str { + fn description(&self) -> &'static str { "Undefined Variable" } - fn documentation(&self) -> &str { + fn documentation(&self) -> &'static str { r"### Example **Incorrect** diff --git a/libs/sqf/src/analyze/lints/s14_unused.rs b/libs/sqf/src/analyze/lints/s14_unused.rs index 852f83e1e..269d67af5 100644 --- a/libs/sqf/src/analyze/lints/s14_unused.rs +++ b/libs/sqf/src/analyze/lints/s14_unused.rs @@ -15,16 +15,16 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS14Unused); impl Lint for LintS14Unused { - fn ident(&self) -> &str { + fn ident(&self) -> &'static str { "unused" } fn sort(&self) -> u32 { 120 } - fn description(&self) -> &str { + fn description(&self) -> &'static str { "Unused Var" } - fn documentation(&self) -> &str { + fn documentation(&self) -> &'static str { r"### Example **Incorrect** diff --git a/libs/sqf/src/analyze/lints/s15_shadowed.rs b/libs/sqf/src/analyze/lints/s15_shadowed.rs index 9f46615e9..eb0b0388c 100644 --- a/libs/sqf/src/analyze/lints/s15_shadowed.rs +++ b/libs/sqf/src/analyze/lints/s15_shadowed.rs @@ -12,16 +12,16 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS15Shadowed); impl Lint for LintS15Shadowed { - fn ident(&self) -> &str { + fn ident(&self) -> &'static str { "shadowed" } fn sort(&self) -> u32 { 150 } - fn description(&self) -> &str { + fn description(&self) -> &'static str { "Shadowed Var" } - fn documentation(&self) -> &str { + fn documentation(&self) -> &'static str { r"### Example **Incorrect** diff --git a/libs/sqf/src/analyze/lints/s16_not_private.rs b/libs/sqf/src/analyze/lints/s16_not_private.rs index fe305e9ea..58fab197a 100644 --- a/libs/sqf/src/analyze/lints/s16_not_private.rs +++ b/libs/sqf/src/analyze/lints/s16_not_private.rs @@ -12,16 +12,16 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS16NotPrivate); impl Lint for LintS16NotPrivate { - fn ident(&self) -> &str { + fn ident(&self) -> &'static str { "not_private" } fn sort(&self) -> u32 { 160 } - fn description(&self) -> &str { + fn description(&self) -> &'static str { "Not Private Var" } - fn documentation(&self) -> &str { + fn documentation(&self) -> &'static str { r"### Example **Incorrect** From 756fc91b48e7dd976759cd2fd5f363b7df90b4f0 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 18 Dec 2024 01:33:10 -0600 Subject: [PATCH 15/24] fmt --- libs/sqf/src/analyze/inspector/commands.rs | 3 ++- libs/sqf/src/analyze/inspector/external_functions.rs | 4 +++- libs/sqf/src/analyze/inspector/game_value.rs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index d955339f9..86aefd08d 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -88,7 +88,8 @@ impl SciptScope { for type_p in &arg_array[2] { if let GameValue::Array(Some(type_array)) = type_p { for type_i in type_array { - var_types.extend(type_i.iter().map(GameValue::make_generic)); + var_types + .extend(type_i.iter().map(GameValue::make_generic)); } } } diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index f96d8b8e5..37f56f595 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -89,7 +89,9 @@ impl SciptScope { } } } - "cba_fnc_addperframehandler" | "cba_fnc_waitandexecute" | "cba_fnc_execnextframe" => { + "cba_fnc_addperframehandler" + | "cba_fnc_waitandexecute" + | "cba_fnc_execnextframe" => { if !gv_array.is_empty() { self.external_new_scope(&gv_array[0], &vec![], database); } diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index aa3fa805d..99b2d254d 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -254,8 +254,8 @@ impl GameValue { Self::ForType(_) => Self::ForType(None), Self::Number(_) => Self::Number(None), Self::String(_) => Self::String(None), - _ => self.clone() - } + _ => self.clone(), + } } #[must_use] /// Get as a string for debugging From 894ac8a075db2e93cc5590f7351614abc1afa445 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 18 Dec 2024 22:25:48 -0600 Subject: [PATCH 16/24] cleanup merge --- libs/sqf/src/analyze/lints/s12_invalid_args.rs | 10 +++++----- libs/sqf/src/analyze/lints/s13_undefined.rs | 10 +++++----- libs/sqf/src/analyze/lints/s14_unused.rs | 10 +++++----- libs/sqf/src/analyze/lints/s15_shadowed.rs | 10 +++++----- libs/sqf/src/analyze/lints/s16_not_private.rs | 10 +++++----- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/libs/sqf/src/analyze/lints/s12_invalid_args.rs b/libs/sqf/src/analyze/lints/s12_invalid_args.rs index acff86ac3..7041b5922 100644 --- a/libs/sqf/src/analyze/lints/s12_invalid_args.rs +++ b/libs/sqf/src/analyze/lints/s12_invalid_args.rs @@ -1,5 +1,5 @@ use crate::{ - analyze::{inspector::Issue, SqfLintData}, + analyze::{inspector::Issue, LintData}, Statements, }; use hemtt_common::config::LintConfig; @@ -11,7 +11,7 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS12InvalidArgs); -impl Lint for LintS12InvalidArgs { +impl Lint for LintS12InvalidArgs { fn ident(&self) -> &'static str { "invalid_args" } @@ -36,13 +36,13 @@ Checks correct syntax usage." fn default_config(&self) -> LintConfig { LintConfig::help() } - fn runners(&self) -> Vec>> { + fn runners(&self) -> Vec>> { vec![Box::new(Runner)] } } pub struct Runner; -impl LintRunner for Runner { +impl LintRunner for Runner { type Target = Statements; fn run( &self, @@ -50,7 +50,7 @@ impl LintRunner for Runner { config: &hemtt_common::config::LintConfig, processed: Option<&hemtt_workspace::reporting::Processed>, target: &Statements, - _data: &SqfLintData, + _data: &LintData, ) -> hemtt_workspace::reporting::Codes { if target.issues().is_empty() { return Vec::new(); diff --git a/libs/sqf/src/analyze/lints/s13_undefined.rs b/libs/sqf/src/analyze/lints/s13_undefined.rs index 9ee4691e8..ca531b9d3 100644 --- a/libs/sqf/src/analyze/lints/s13_undefined.rs +++ b/libs/sqf/src/analyze/lints/s13_undefined.rs @@ -1,5 +1,5 @@ use crate::{ - analyze::{inspector::Issue, SqfLintData}, + analyze::{inspector::Issue, LintData}, Statements, }; use hemtt_common::config::LintConfig; @@ -11,7 +11,7 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS13Undefined); -impl Lint for LintS13Undefined { +impl Lint for LintS13Undefined { fn ident(&self) -> &'static str { "undefined" } @@ -36,13 +36,13 @@ Checks correct syntax usage." fn default_config(&self) -> LintConfig { LintConfig::help() } - fn runners(&self) -> Vec>> { + fn runners(&self) -> Vec>> { vec![Box::new(Runner)] } } pub struct Runner; -impl LintRunner for Runner { +impl LintRunner for Runner { type Target = Statements; fn run( &self, @@ -50,7 +50,7 @@ impl LintRunner for Runner { config: &hemtt_common::config::LintConfig, processed: Option<&hemtt_workspace::reporting::Processed>, target: &Statements, - _data: &SqfLintData, + _data: &LintData, ) -> hemtt_workspace::reporting::Codes { if target.issues().is_empty() { return Vec::new(); diff --git a/libs/sqf/src/analyze/lints/s14_unused.rs b/libs/sqf/src/analyze/lints/s14_unused.rs index 269d67af5..9a17be1e8 100644 --- a/libs/sqf/src/analyze/lints/s14_unused.rs +++ b/libs/sqf/src/analyze/lints/s14_unused.rs @@ -1,7 +1,7 @@ use crate::{ analyze::{ inspector::{Issue, VarSource}, - SqfLintData, + LintData, }, Statements, }; @@ -14,7 +14,7 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS14Unused); -impl Lint for LintS14Unused { +impl Lint for LintS14Unused { fn ident(&self) -> &'static str { "unused" } @@ -39,13 +39,13 @@ Checks for vars that are never used." fn default_config(&self) -> LintConfig { LintConfig::help().with_enabled(false) } - fn runners(&self) -> Vec>> { + fn runners(&self) -> Vec>> { vec![Box::new(Runner)] } } pub struct Runner; -impl LintRunner for Runner { +impl LintRunner for Runner { type Target = Statements; fn run( &self, @@ -53,7 +53,7 @@ impl LintRunner for Runner { config: &hemtt_common::config::LintConfig, processed: Option<&hemtt_workspace::reporting::Processed>, target: &Statements, - _data: &SqfLintData, + _data: &LintData, ) -> hemtt_workspace::reporting::Codes { if target.issues().is_empty() { return Vec::new(); diff --git a/libs/sqf/src/analyze/lints/s15_shadowed.rs b/libs/sqf/src/analyze/lints/s15_shadowed.rs index eb0b0388c..d5370b0a5 100644 --- a/libs/sqf/src/analyze/lints/s15_shadowed.rs +++ b/libs/sqf/src/analyze/lints/s15_shadowed.rs @@ -1,5 +1,5 @@ use crate::{ - analyze::{inspector::Issue, SqfLintData}, + analyze::{inspector::Issue, LintData}, Statements, }; use hemtt_common::config::LintConfig; @@ -11,7 +11,7 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS15Shadowed); -impl Lint for LintS15Shadowed { +impl Lint for LintS15Shadowed { fn ident(&self) -> &'static str { "shadowed" } @@ -37,13 +37,13 @@ Checks for variables being shadowed." fn default_config(&self) -> LintConfig { LintConfig::help().with_enabled(false) } - fn runners(&self) -> Vec>> { + fn runners(&self) -> Vec>> { vec![Box::new(Runner)] } } pub struct Runner; -impl LintRunner for Runner { +impl LintRunner for Runner { type Target = Statements; fn run( &self, @@ -51,7 +51,7 @@ impl LintRunner for Runner { config: &hemtt_common::config::LintConfig, processed: Option<&hemtt_workspace::reporting::Processed>, target: &Statements, - _data: &SqfLintData, + _data: &LintData, ) -> hemtt_workspace::reporting::Codes { if target.issues().is_empty() { return Vec::new(); diff --git a/libs/sqf/src/analyze/lints/s16_not_private.rs b/libs/sqf/src/analyze/lints/s16_not_private.rs index 58fab197a..2e83481e0 100644 --- a/libs/sqf/src/analyze/lints/s16_not_private.rs +++ b/libs/sqf/src/analyze/lints/s16_not_private.rs @@ -1,5 +1,5 @@ use crate::{ - analyze::{inspector::Issue, SqfLintData}, + analyze::{inspector::Issue, LintData}, Statements, }; use hemtt_common::config::LintConfig; @@ -11,7 +11,7 @@ use std::{ops::Range, sync::Arc}; crate::analyze::lint!(LintS16NotPrivate); -impl Lint for LintS16NotPrivate { +impl Lint for LintS16NotPrivate { fn ident(&self) -> &'static str { "not_private" } @@ -36,13 +36,13 @@ Checks local variables that are not private." fn default_config(&self) -> LintConfig { LintConfig::help().with_enabled(false) } - fn runners(&self) -> Vec>> { + fn runners(&self) -> Vec>> { vec![Box::new(Runner)] } } pub struct Runner; -impl LintRunner for Runner { +impl LintRunner for Runner { type Target = Statements; fn run( &self, @@ -50,7 +50,7 @@ impl LintRunner for Runner { config: &hemtt_common::config::LintConfig, processed: Option<&hemtt_workspace::reporting::Processed>, target: &Statements, - _data: &SqfLintData, + _data: &LintData, ) -> hemtt_workspace::reporting::Codes { if target.issues().is_empty() { return Vec::new(); From 68d8a7255d84a075a56bd1040c0bf7b27f697aaf Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Tue, 7 Jan 2025 15:41:17 -0600 Subject: [PATCH 17/24] Support `_actionParams` in ace_interact_menu_fnc_createaction ref https://github.com/acemod/ACE3/pull/10527 --- libs/sqf/src/analyze/inspector/external_functions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index 37f56f595..a721c40b1 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -83,6 +83,7 @@ impl SciptScope { &vec![ ("_target", GameValue::Object), ("_player", GameValue::Object), + ("_actionParams", GameValue::Anything), ], database, ); From c8161415b26e4d3438b31a5665420802a25326af Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Thu, 6 Feb 2025 17:38:08 -0600 Subject: [PATCH 18/24] Fix merge --- libs/sqf/src/analyze/lints/s14_unused.rs | 4 ++-- libs/sqf/src/analyze/lints/s15_shadowed.rs | 4 ++-- libs/sqf/src/analyze/lints/s16_not_private.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/sqf/src/analyze/lints/s14_unused.rs b/libs/sqf/src/analyze/lints/s14_unused.rs index 9a17be1e8..9d032b13b 100644 --- a/libs/sqf/src/analyze/lints/s14_unused.rs +++ b/libs/sqf/src/analyze/lints/s14_unused.rs @@ -5,7 +5,7 @@ use crate::{ }, Statements, }; -use hemtt_common::config::LintConfig; +use hemtt_common::config::{LintConfig, LintEnabled}; use hemtt_workspace::{ lint::{AnyLintRunner, Lint, LintRunner}, reporting::{Code, Codes, Diagnostic, Processed, Severity}, @@ -37,7 +37,7 @@ private _z = 5; // and never used Checks for vars that are never used." } fn default_config(&self) -> LintConfig { - LintConfig::help().with_enabled(false) + LintConfig::help().with_enabled(LintEnabled::Pedantic) } fn runners(&self) -> Vec>> { vec![Box::new(Runner)] diff --git a/libs/sqf/src/analyze/lints/s15_shadowed.rs b/libs/sqf/src/analyze/lints/s15_shadowed.rs index d5370b0a5..f24e6569f 100644 --- a/libs/sqf/src/analyze/lints/s15_shadowed.rs +++ b/libs/sqf/src/analyze/lints/s15_shadowed.rs @@ -2,7 +2,7 @@ use crate::{ analyze::{inspector::Issue, LintData}, Statements, }; -use hemtt_common::config::LintConfig; +use hemtt_common::config::{LintConfig, LintEnabled}; use hemtt_workspace::{ lint::{AnyLintRunner, Lint, LintRunner}, reporting::{Code, Codes, Diagnostic, Processed, Severity}, @@ -35,7 +35,7 @@ private _z = 5; Checks for variables being shadowed." } fn default_config(&self) -> LintConfig { - LintConfig::help().with_enabled(false) + LintConfig::help().with_enabled(LintEnabled::Pedantic) } fn runners(&self) -> Vec>> { vec![Box::new(Runner)] diff --git a/libs/sqf/src/analyze/lints/s16_not_private.rs b/libs/sqf/src/analyze/lints/s16_not_private.rs index 2e83481e0..fb703a726 100644 --- a/libs/sqf/src/analyze/lints/s16_not_private.rs +++ b/libs/sqf/src/analyze/lints/s16_not_private.rs @@ -2,7 +2,7 @@ use crate::{ analyze::{inspector::Issue, LintData}, Statements, }; -use hemtt_common::config::LintConfig; +use hemtt_common::config::{LintConfig, LintEnabled}; use hemtt_workspace::{ lint::{AnyLintRunner, Lint, LintRunner}, reporting::{Code, Codes, Diagnostic, Processed, Severity}, @@ -34,7 +34,7 @@ _z = 6; Checks local variables that are not private." } fn default_config(&self) -> LintConfig { - LintConfig::help().with_enabled(false) + LintConfig::help().with_enabled(LintEnabled::Pedantic) } fn runners(&self) -> Vec>> { vec![Box::new(Runner)] From 1dbf7f2a14cdf00ae577908e896c056525ffd1de Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sat, 8 Feb 2025 12:53:24 -0600 Subject: [PATCH 19/24] Check array subtypes (pos ASL vs AGL) --- libs/sqf/src/analyze/inspector/commands.rs | 16 ++-- .../analyze/inspector/external_functions.rs | 3 +- libs/sqf/src/analyze/inspector/game_value.rs | 81 ++++++++++++++----- libs/sqf/src/analyze/inspector/mod.rs | 2 +- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index 86aefd08d..7d5e8335c 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -27,7 +27,7 @@ impl SciptScope { } } for possible in rhs { - if let GameValue::Array(Some(gv_array)) = possible { + if let GameValue::Array(Some(gv_array), _) = possible { for gv_index in gv_array { for element in gv_index { let GameValue::String(Some(Expression::String(var, source, _))) = element @@ -53,7 +53,7 @@ impl SciptScope { #[must_use] pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { for possible in rhs { - let GameValue::Array(Some(gv_array)) = possible else { + let GameValue::Array(Some(gv_array), _) = possible else { continue; }; @@ -71,7 +71,7 @@ impl SciptScope { VarSource::Params(source.clone()), ); } - GameValue::Array(Some(arg_array)) => { + GameValue::Array(Some(arg_array), _) => { if arg_array.is_empty() || arg_array[0].is_empty() { continue; } @@ -86,7 +86,7 @@ impl SciptScope { let mut var_types = HashSet::new(); if arg_array.len() > 2 { for type_p in &arg_array[2] { - if let GameValue::Array(Some(type_array)) = type_p { + if let GameValue::Array(Some(type_array), _) = type_p { for type_i in type_array { var_types .extend(type_i.iter().map(GameValue::make_generic)); @@ -240,7 +240,7 @@ impl SciptScope { }; possible_array.push(expression.clone()); } - GameValue::Array(option) => { + GameValue::Array(option, _) => { let Some(for_stages) = option else { return_value.insert(GameValue::ForType(None)); continue; @@ -310,7 +310,7 @@ impl SciptScope { if let GameValue::Code(Some(Expression::Code(_statements))) = possible { return_value.extend(self.cmd_generic_call(rhs, database)); } - if let GameValue::Array(Some(gv_array)) = possible { + if let GameValue::Array(Some(gv_array), _) = possible { for gv_index in gv_array { for element in gv_index { if let GameValue::Code(Some(expression)) = element { @@ -348,7 +348,7 @@ impl SciptScope { ) -> HashSet { let mut possible_code = HashSet::new(); for possible_outer in rhs { - let GameValue::Array(Some(gv_array)) = possible_outer else { + let GameValue::Array(Some(gv_array), _) = possible_outer else { continue; }; if gv_array.len() < 2 { @@ -392,7 +392,7 @@ impl SciptScope { .iter() .any(|r| matches!(r, GameValue::Boolean(..)) || matches!(r, GameValue::Number(..))) { - if let Some(GameValue::Array(Some(gv_array))) = lhs.iter().next() { + if let Some(GameValue::Array(Some(gv_array), _)) = lhs.iter().next() { // return_value.clear(); // todo: could clear if we handle pushBack for gv_index in gv_array { for element in gv_index { diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index a721c40b1..dcb6169aa 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -29,7 +29,8 @@ impl SciptScope { ); } } - GameValue::Array(Some(gv_array)) => match ext_func.to_ascii_lowercase().as_str() { + GameValue::Array(Some(gv_array), _) => match ext_func.to_ascii_lowercase().as_str() + { // Functions that will run in existing scope "cba_fnc_hasheachpair" | "cba_fnc_hashfilter" => { if gv_array.len() > 1 { diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index 99b2d254d..a2ab525b2 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -11,7 +11,7 @@ use crate::{parser::database::Database, Expression}; pub enum GameValue { Anything, // Assignment, // as in z = call {x=1}??? - Array(Option>>), + Array(Option>>, Option), Boolean(Option), Code(Option), Config, @@ -37,6 +37,20 @@ pub enum GameValue { WithType, } +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum ArrayType { + Color, + // Pos2d, + PosAGL, + PosAGLS, + PosASL, + PosASLW, + PosATL, + /// from object's center `getPosWorld` + PosWorld, + PosRelative, +} + impl GameValue { #[must_use] /// Gets cmd return types based on input types @@ -93,8 +107,19 @@ impl GameValue { } } let value = &syntax.ret().0; + + let temp_testing_type = match cmd_name { + "getPosASL" => &Value::Position3dASL, + "ASLToAGL" | "AGLToASL" => &Value::Position3dAGL, + _ => value, + }; + if temp_testing_type != temp_testing_type { + // println!("modifying {cmd_name} output {:?} -> {:?}", value, temp_testing_type); + } + + let game_value = Self::from_wiki_value(temp_testing_type); // println!("match syntax {syntax:?}"); - return_types.insert(Self::from_wiki_value(value)); + return_types.insert(game_value); } // println!("lhs_set {lhs_set:?}"); // println!("rhs_set {rhs_set:?}"); @@ -135,12 +160,20 @@ impl GameValue { } return true; }; + let temp_testing_type = match cmd_name { + "ASLToAGL" => &Value::Position3dASL, + "AGLToASL" => &Value::Position3dAGL, + _ => param.typ(), + }; + if temp_testing_type != param.typ() { + // println!("modifying input {:?} -> {:?}", param.typ(), temp_testing_type); + } // println!( // "[arg {name}] typ: {:?}, opt: {:?}", - // param.typ(), + // temp_testing_type, // param.optional() // ); - Self::match_set_to_value(set, param.typ(), param.optional()) + Self::match_set_to_value(set, temp_testing_type, param.optional()) } Arg::Array(arg_array) => { const WIKI_CMDS_IGNORE_ARGS: &[&str] = &["createHashMapFromArray"]; @@ -150,11 +183,11 @@ impl GameValue { set.iter().any(|s| { match s { - Self::Anything | Self::Array(None) => { + Self::Anything | Self::Array(None, _) => { // println!("array (any/generic) pass"); true } - Self::Array(Some(gv_array)) => { + Self::Array(Some(gv_array), _) => { // println!("array (gv: {}) expected (arg: {})", gv_array.len(), arg_array.len()); // note: some syntaxes take more than others for (index, arg) in arg_array.iter().enumerate() { @@ -189,11 +222,14 @@ impl GameValue { #[must_use] /// matches values are compatible (Anything will always match) pub fn match_values(left: &Self, right: &Self) -> bool { - if matches!(left, Self::Anything) { + if matches!(left, Self::Anything) || matches!(right, Self::Anything) { return true; } - if matches!(right, Self::Anything) { - return true; + if let (Self::Array(_, Some(lpos)), Self::Array(_, Some(rpos))) = (left, right) { + if lpos != rpos { + println!("array fail {:?}!={:?}", lpos, rpos); + return false; + } } std::mem::discriminant(left) == std::mem::discriminant(right) } @@ -210,8 +246,13 @@ impl GameValue { | Value::ArraySized { .. } | Value::ArrayUnknown | Value::ArrayUnsized { .. } - | Value::Position - | Value::Waypoint => Self::Array(None), + | Value::Position // position is often too generic to match? + | Value::Waypoint => Self::Array(None, None), + Value::Position3dAGL => Self::Array(None, Some(ArrayType::PosAGL)), + Value::Position3dAGLS => Self::Array(None, Some(ArrayType::PosAGLS)), + Value::Position3dASL => Self::Array(None, Some(ArrayType::PosASL)), + Value::Position3DASLW => Self::Array(None, Some(ArrayType::PosASLW)), + Value::Position3dATL => Self::Array(None, Some(ArrayType::PosATL)), Value::Boolean => Self::Boolean(None), Value::Code => Self::Code(None), Value::Config => Self::Config, @@ -248,7 +289,7 @@ impl GameValue { /// Gets a generic version of a type pub fn make_generic(&self) -> Self { match self { - Self::Array(_) => Self::Array(None), + Self::Array(_, _) => Self::Array(None, None), Self::Boolean(_) => Self::Boolean(None), Self::Code(_) => Self::Code(None), Self::ForType(_) => Self::ForType(None), @@ -293,14 +334,14 @@ impl GameValue { "Boolean(GENERIC)".to_string() } } - Self::Array(gv_array_option) => - { - #[allow(clippy::option_if_let_else)] - if let Some(gv_array) = gv_array_option { - format!("ArrayExp(len {})", gv_array.len()) - } else { - "ArrayExp(GENERIC)".to_string() - } + Self::Array(gv_array_option, position_option) => { + let str_len = gv_array_option + .clone() + .map_or("GENERIC".to_string(), |l| format!("len {}", l.len())); + let str_pos = position_option + .clone() + .map_or("".to_string(), |p| format!(":{p:?}")); + format!("ArrayExp({str_len}{str_pos})") } Self::Code(expression) => { if let Some(Expression::Code(statements)) = expression { diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 0313a736b..68ea7ec24 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -256,7 +256,7 @@ impl SciptScope { .iter() .map(|e| self.eval_expression(e, database).into_iter().collect()) .collect(); - HashSet::from([GameValue::Array(Some(gv_array))]) + HashSet::from([GameValue::Array(Some(gv_array), None)]) } Expression::NularCommand(cmd, source) => { debug_type = format!("[N:{}]", cmd.as_str()); From 5e8d88101e38f82f010818221bbbf721b6ba8a1b Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sat, 8 Feb 2025 23:33:47 -0600 Subject: [PATCH 20/24] StructuredText --- libs/sqf/src/analyze/inspector/game_value.rs | 2 ++ libs/sqf/src/analyze/inspector/mod.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index a2ab525b2..15f3ba679 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -30,6 +30,7 @@ pub enum GameValue { ScriptHandle, Side, String(Option), + StructuredText, SwitchType, Task, TeamMember, @@ -271,6 +272,7 @@ impl GameValue { Value::Side => Self::Side, Value::String => Self::String(None), Value::SwitchType => Self::SwitchType, + Value::StructuredText => Self::StructuredText, Value::Task => Self::Task, Value::TeamMember => Self::TeamMember, Value::WhileType => Self::WhileType, diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index 68ea7ec24..aac921941 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -308,7 +308,7 @@ impl SciptScope { } let return_set = match cmd { BinaryCommand::Associate => { - // the : from case + // the : from case ToDo: these run outside of the do scope let _ = self.cmd_generic_call(&rhs_set, database); None } From efb1e3d509073d055218c6d04a12125abc9ce966 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Mon, 10 Feb 2025 23:25:10 -0600 Subject: [PATCH 21/24] Improve global scope, use indexhash, modify l-value arrays --- libs/sqf/Cargo.toml | 1 + libs/sqf/src/analyze/inspector/commands.rs | 119 +++++++++------- .../analyze/inspector/external_functions.rs | 20 ++- libs/sqf/src/analyze/inspector/game_value.rs | 45 +++--- libs/sqf/src/analyze/inspector/mod.rs | 130 +++++++++++------- 5 files changed, 194 insertions(+), 121 deletions(-) diff --git a/libs/sqf/Cargo.toml b/libs/sqf/Cargo.toml index 80e56983a..5de2f15a6 100644 --- a/libs/sqf/Cargo.toml +++ b/libs/sqf/Cargo.toml @@ -22,6 +22,7 @@ byteorder = { workspace = true, optional = true } chumsky = { workspace = true, optional = true} float-ord = "0.3.2" linkme = { workspace = true } +indexmap = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } diff --git a/libs/sqf/src/analyze/inspector/commands.rs b/libs/sqf/src/analyze/inspector/commands.rs index 7d5e8335c..174cb077f 100644 --- a/libs/sqf/src/analyze/inspector/commands.rs +++ b/libs/sqf/src/analyze/inspector/commands.rs @@ -1,6 +1,8 @@ //! Emulates engine commands -use std::{collections::HashSet, ops::Range}; +use std::ops::Range; + +use indexmap::IndexSet; use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; @@ -8,20 +10,20 @@ use super::{game_value::GameValue, SciptScope}; impl SciptScope { #[must_use] - pub fn cmd_u_private(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_u_private(&mut self, rhs: &IndexSet) -> IndexSet { fn push_var(s: &mut SciptScope, var: &String, source: &Range) { if s.ignored_vars.contains(&var.to_ascii_lowercase()) { s.var_assign( &var.to_string(), true, - HashSet::from([GameValue::Anything]), + IndexSet::from([GameValue::Anything]), VarSource::Ignore, ); } else { s.var_assign( &var.to_string(), true, - HashSet::from([GameValue::Nothing]), + IndexSet::from([GameValue::Nothing]), VarSource::Private(source.clone()), ); } @@ -48,10 +50,10 @@ impl SciptScope { push_var(self, &var.to_string(), source); } } - HashSet::new() + IndexSet::new() } #[must_use] - pub fn cmd_generic_params(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_generic_params(&mut self, rhs: &IndexSet) -> IndexSet { for possible in rhs { let GameValue::Array(Some(gv_array), _) = possible else { continue; @@ -67,7 +69,7 @@ impl SciptScope { self.var_assign( var.as_ref(), true, - HashSet::from([GameValue::Anything]), + IndexSet::from([GameValue::Anything]), VarSource::Params(source.clone()), ); } @@ -83,7 +85,7 @@ impl SciptScope { if var_name.is_empty() { continue; } - let mut var_types = HashSet::new(); + let mut var_types = IndexSet::new(); if arg_array.len() > 2 { for type_p in &arg_array[2] { if let GameValue::Array(Some(type_array), _) = type_p { @@ -115,14 +117,14 @@ impl SciptScope { } } } - HashSet::from([GameValue::Boolean(None)]) + IndexSet::from([GameValue::Boolean(None)]) } #[must_use] pub fn cmd_generic_call( &mut self, - rhs: &HashSet, + rhs: &IndexSet, database: &Database, - ) -> HashSet { + ) -> IndexSet { for possible in rhs { let GameValue::Code(Some(expression)) = possible else { continue; @@ -138,15 +140,15 @@ impl SciptScope { self.eval_statements(statements, database); self.pop(); } - HashSet::from([GameValue::Anything]) + IndexSet::from([GameValue::Anything]) } #[must_use] pub fn cmd_b_do( &mut self, - lhs: &HashSet, - rhs: &HashSet, + lhs: &IndexSet, + rhs: &IndexSet, database: &Database, - ) -> HashSet { + ) -> IndexSet { for possible in rhs { let GameValue::Code(Some(expression)) = possible else { continue; @@ -171,7 +173,7 @@ impl SciptScope { self.var_assign( var.as_ref(), true, - HashSet::from([GameValue::Number(None)]), + IndexSet::from([GameValue::Number(None)]), VarSource::ForLoop(source.clone()), ); } @@ -192,16 +194,16 @@ impl SciptScope { } self.pop(); } - HashSet::from([GameValue::Anything]) + IndexSet::from([GameValue::Anything]) } #[must_use] pub fn cmd_generic_call_magic( &mut self, - code_possibilities: &HashSet, + code_possibilities: &IndexSet, magic: &Vec<(&str, GameValue)>, source: &Range, database: &Database, - ) -> HashSet { + ) -> IndexSet { for possible in code_possibilities { let GameValue::Code(Some(expression)) = possible else { continue; @@ -217,7 +219,7 @@ impl SciptScope { self.var_assign( var, true, - HashSet::from([value.clone()]), + IndexSet::from([value.clone()]), VarSource::Magic(source.clone()), ); } @@ -225,11 +227,11 @@ impl SciptScope { self.eval_statements(statements, database); self.pop(); } - HashSet::from([GameValue::Anything]) + IndexSet::from([GameValue::Anything]) } #[must_use] - pub fn cmd_for(&mut self, rhs: &HashSet) -> HashSet { - let mut return_value = HashSet::new(); + pub fn cmd_for(&mut self, rhs: &IndexSet) -> IndexSet { + let mut return_value = IndexSet::new(); for possible in rhs { let mut possible_array = Vec::new(); match possible { @@ -268,17 +270,17 @@ impl SciptScope { /// for (from, to, step) chained commands pub fn cmd_b_from_chain( &self, - lhs: &HashSet, - _rhs: &HashSet, - ) -> HashSet { + lhs: &IndexSet, + _rhs: &IndexSet, + ) -> IndexSet { lhs.clone() } #[must_use] pub fn cmd_u_is_nil( &mut self, - rhs: &HashSet, + rhs: &IndexSet, database: &Database, - ) -> HashSet { + ) -> IndexSet { let mut non_string = false; for possible in rhs { let GameValue::String(possible) = possible else { @@ -296,16 +298,16 @@ impl SciptScope { if non_string { let _ = self.cmd_generic_call(rhs, database); } - HashSet::from([GameValue::Boolean(None)]) + IndexSet::from([GameValue::Boolean(None)]) } #[must_use] pub fn cmd_b_then( &mut self, - _lhs: &HashSet, - rhs: &HashSet, + _lhs: &IndexSet, + rhs: &IndexSet, database: &Database, - ) -> HashSet { - let mut return_value = HashSet::new(); + ) -> IndexSet { + let mut return_value = IndexSet::new(); for possible in rhs { if let GameValue::Code(Some(Expression::Code(_statements))) = possible { return_value.extend(self.cmd_generic_call(rhs, database)); @@ -315,7 +317,7 @@ impl SciptScope { for element in gv_index { if let GameValue::Code(Some(expression)) = element { return_value.extend(self.cmd_generic_call( - &HashSet::from([GameValue::Code(Some(expression.clone()))]), + &IndexSet::from([GameValue::Code(Some(expression.clone()))]), database, )); } @@ -328,10 +330,10 @@ impl SciptScope { #[must_use] pub fn cmd_b_else( &self, - lhs: &HashSet, - rhs: &HashSet, - ) -> HashSet { - let mut return_value = HashSet::new(); // just merge, not really the same but should be fine + lhs: &IndexSet, + rhs: &IndexSet, + ) -> IndexSet { + let mut return_value = IndexSet::new(); // just merge, not really the same but should be fine for possible in rhs { return_value.insert(possible.clone()); } @@ -343,10 +345,10 @@ impl SciptScope { #[must_use] pub fn cmd_b_get_or_default_call( &mut self, - rhs: &HashSet, + rhs: &IndexSet, database: &Database, - ) -> HashSet { - let mut possible_code = HashSet::new(); + ) -> IndexSet { + let mut possible_code = IndexSet::new(); for possible_outer in rhs { let GameValue::Array(Some(gv_array), _) = possible_outer else { continue; @@ -357,10 +359,10 @@ impl SciptScope { possible_code.extend(gv_array[1].clone()); } let _ = self.cmd_generic_call(&possible_code, database); - HashSet::from([GameValue::Anything]) + IndexSet::from([GameValue::Anything]) } #[must_use] - pub fn cmd_u_to_string(&mut self, rhs: &HashSet) -> HashSet { + pub fn cmd_u_to_string(&mut self, rhs: &IndexSet) -> IndexSet { for possible in rhs { let GameValue::Code(Some(expression)) = possible else { continue; @@ -371,17 +373,17 @@ impl SciptScope { // just skip because it will often use a _x self.code_used.insert(expression.clone()); } - HashSet::from([GameValue::String(None)]) + IndexSet::from([GameValue::String(None)]) } #[must_use] pub fn cmd_b_select( &mut self, - lhs: &HashSet, - rhs: &HashSet, - cmd_set: &HashSet, + lhs: &IndexSet, + rhs: &IndexSet, + cmd_set: &IndexSet, source: &Range, database: &Database, - ) -> HashSet { + ) -> IndexSet { let mut return_value = cmd_set.clone(); // Check: `array select expression` let _ = @@ -403,4 +405,25 @@ impl SciptScope { } return_value } + /// emulate a possibly modified l-value array by a command + pub fn cmd_generic_modify_lvalue(&mut self, lhs: &Expression) { + let Expression::Variable(var_name, _) = lhs else { + return; + }; + // if var currently contains a specialized array + if !self + .var_retrieve(var_name, &lhs.full_span(), true) + .iter() + .any(|v| matches!(v, GameValue::Array(Some(_), _))) + { + return; + } + // push a generic array + self.var_assign( + var_name, + false, + IndexSet::from([GameValue::Array(None, None)]), + VarSource::Ignore, + ); + } } diff --git a/libs/sqf/src/analyze/inspector/external_functions.rs b/libs/sqf/src/analyze/inspector/external_functions.rs index dcb6169aa..b54febb1b 100644 --- a/libs/sqf/src/analyze/inspector/external_functions.rs +++ b/libs/sqf/src/analyze/inspector/external_functions.rs @@ -1,6 +1,6 @@ //! Emulate how common external functions will handle code -use std::collections::HashSet; +use indexmap::IndexSet; use crate::{analyze::inspector::VarSource, parser::database::Database, Expression}; @@ -10,7 +10,7 @@ impl SciptScope { #[allow(clippy::too_many_lines)] pub fn external_function( &mut self, - lhs: &HashSet, + lhs: &IndexSet, rhs: &Expression, database: &Database, ) { @@ -153,10 +153,15 @@ impl SciptScope { if self.code_used.contains(expression) { return; } - let mut ext_scope = Self::create(&self.ignored_vars, false); + let mut ext_scope = Self::create(self.global.clone(), &self.ignored_vars, false); for (var, value) in vars { - ext_scope.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); + ext_scope.var_assign( + var, + true, + IndexSet::from([value.clone()]), + VarSource::Ignore, + ); } self.code_used.insert(expression.clone()); ext_scope.eval_statements(statements, database); @@ -181,7 +186,12 @@ impl SciptScope { } self.push(); for (var, value) in vars { - self.var_assign(var, true, HashSet::from([value.clone()]), VarSource::Ignore); + self.var_assign( + var, + true, + IndexSet::from([value.clone()]), + VarSource::Ignore, + ); } self.code_used.insert(expression.clone()); self.eval_statements(statements, database); diff --git a/libs/sqf/src/analyze/inspector/game_value.rs b/libs/sqf/src/analyze/inspector/game_value.rs index 15f3ba679..5fb01ba0e 100644 --- a/libs/sqf/src/analyze/inspector/game_value.rs +++ b/libs/sqf/src/analyze/inspector/game_value.rs @@ -1,8 +1,7 @@ //! Game Values and mapping them from commands -use std::collections::HashSet; - use arma3_wiki::model::{Arg, Call, Param, Value}; +use indexmap::IndexSet; use tracing::{trace, warn}; use crate::{parser::database::Database, Expression}; @@ -57,15 +56,15 @@ impl GameValue { /// Gets cmd return types based on input types pub fn from_cmd( expression: &Expression, - lhs_set: Option<&HashSet>, - rhs_set: Option<&HashSet>, + lhs_set: Option<&IndexSet>, + rhs_set: Option<&IndexSet>, database: &Database, - ) -> HashSet { - let mut return_types = HashSet::new(); + ) -> IndexSet { + let mut return_types = IndexSet::new(); let cmd_name = expression.command_name().expect("has a name"); let Some(command) = database.wiki().commands().get(cmd_name) else { println!("cmd {cmd_name} not in db?"); - return HashSet::from([Self::Anything]); + return IndexSet::from([Self::Anything]); }; for syntax in command.syntax() { @@ -109,13 +108,14 @@ impl GameValue { } let value = &syntax.ret().0; + #[allow(clippy::match_single_binding)] let temp_testing_type = match cmd_name { - "getPosASL" => &Value::Position3dASL, - "ASLToAGL" | "AGLToASL" => &Value::Position3dAGL, + // "AGLToASL" | "getPosASL" => &Value::Position3dASL, + // "ASLToAGL" | => &Value::Position3dAGL, _ => value, }; - if temp_testing_type != temp_testing_type { - // println!("modifying {cmd_name} output {:?} -> {:?}", value, temp_testing_type); + if value != temp_testing_type { + println!("modifying {cmd_name} output {value:?} -> {temp_testing_type:?}"); } let game_value = Self::from_wiki_value(temp_testing_type); @@ -136,7 +136,7 @@ impl GameValue { #[must_use] pub fn match_set_to_arg( cmd_name: &str, - set: &HashSet, + set: &IndexSet, arg: &Arg, params: &[Param], ) -> bool { @@ -161,13 +161,14 @@ impl GameValue { } return true; }; + #[allow(clippy::match_single_binding)] let temp_testing_type = match cmd_name { - "ASLToAGL" => &Value::Position3dASL, - "AGLToASL" => &Value::Position3dAGL, + // "ASLToAGL" => &Value::Position3dASL, + // "AGLToASL" => &Value::Position3dAGL, _ => param.typ(), }; if temp_testing_type != param.typ() { - // println!("modifying input {:?} -> {:?}", param.typ(), temp_testing_type); + println!("modifying input {:?} -> {temp_testing_type:?}", param.typ()); } // println!( // "[arg {name}] typ: {:?}, opt: {:?}", @@ -195,7 +196,7 @@ impl GameValue { let possible = if index < gv_array.len() { gv_array[index].iter().cloned().collect() } else { - HashSet::new() + IndexSet::new() }; if !Self::match_set_to_arg(cmd_name, &possible, arg, params) { return false; @@ -211,7 +212,7 @@ impl GameValue { } #[must_use] - pub fn match_set_to_value(set: &HashSet, right_wiki: &Value, optional: bool) -> bool { + pub fn match_set_to_value(set: &IndexSet, right_wiki: &Value, optional: bool) -> bool { // println!("Checking {:?} against {:?} [O:{optional}]", set, right_wiki); if optional && (set.is_empty() || set.contains(&Self::Nothing)) { return true; @@ -228,8 +229,9 @@ impl GameValue { } if let (Self::Array(_, Some(lpos)), Self::Array(_, Some(rpos))) = (left, right) { if lpos != rpos { - println!("array fail {:?}!={:?}", lpos, rpos); - return false; + // ToDo: Handle matchign array types better eg: AGLS vs AGL + // println!("array mismatch {lpos:?}!={rpos:?}"); + // return false; } } std::mem::discriminant(left) == std::mem::discriminant(right) @@ -261,8 +263,9 @@ impl GameValue { Value::DiaryRecord => Self::DiaryRecord, Value::Display => Self::Display, Value::ForType => Self::ForType(None), - Value::IfType => Self::IfType, Value::Group => Self::Group, + Value::HashMapUnknown => Self::HashMap, + Value::IfType => Self::IfType, Value::Location => Self::Location, Value::Namespace => Self::Namespace, Value::Nothing => Self::Nothing, @@ -342,7 +345,7 @@ impl GameValue { .map_or("GENERIC".to_string(), |l| format!("len {}", l.len())); let str_pos = position_option .clone() - .map_or("".to_string(), |p| format!(":{p:?}")); + .map_or(String::new(), |p| format!(":{p:?}")); format!("ArrayExp({str_len}{str_pos})") } Self::Code(expression) => { diff --git a/libs/sqf/src/analyze/inspector/mod.rs b/libs/sqf/src/analyze/inspector/mod.rs index aac921941..9e217eb8d 100644 --- a/libs/sqf/src/analyze/inspector/mod.rs +++ b/libs/sqf/src/analyze/inspector/mod.rs @@ -1,19 +1,15 @@ //! Inspects code, checking code args and variable usage //! -use std::{ - collections::{HashMap, HashSet}, - hash::Hash, - ops::Range, - vec, -}; +use std::{cell::RefCell, hash::Hash, ops::Range, rc::Rc, vec}; use crate::{ parser::database::Database, BinaryCommand, Expression, Statement, Statements, UnaryCommand, }; use game_value::GameValue; use hemtt_workspace::reporting::Processed; +use indexmap::{IndexMap, IndexSet}; use regex::Regex; -use tracing::{error, trace}; +use tracing::trace; mod commands; mod external_functions; @@ -57,34 +53,38 @@ impl VarSource { #[derive(Debug, Clone, PartialEq, Eq)] pub struct VarHolder { - possible: HashSet, + possible: IndexSet, usage: i32, source: VarSource, } -pub type Stack = HashMap; +pub type Stack = IndexMap; pub struct SciptScope { - errors: HashSet, - global: Stack, + errors: IndexSet, + global: Rc>, local: Vec, - code_seen: HashSet, - code_used: HashSet, + code_seen: IndexSet, + code_used: IndexSet, /// Orphan scopes are code blocks that are created but don't appear to be called in a known way is_orphan_scope: bool, - ignored_vars: HashSet, + ignored_vars: IndexSet, } impl SciptScope { #[must_use] - pub fn create(ignored_vars: &HashSet, is_orphan_scope: bool) -> Self { + pub fn create( + global: Rc>, + ignored_vars: &IndexSet, + is_orphan_scope: bool, + ) -> Self { // trace!("Creating ScriptScope"); let mut scope = Self { - errors: HashSet::new(), - global: Stack::new(), + errors: IndexSet::new(), + global, local: Vec::new(), - code_seen: HashSet::new(), - code_used: HashSet::new(), + code_seen: IndexSet::new(), + code_used: IndexSet::new(), is_orphan_scope, ignored_vars: ignored_vars.clone(), }; @@ -93,30 +93,30 @@ impl SciptScope { scope.var_assign( var, true, - HashSet::from([GameValue::Anything]), + IndexSet::from([GameValue::Anything]), VarSource::Ignore, ); } scope } #[must_use] - pub fn finish(&mut self, check_child_scripts: bool, database: &Database) -> HashSet { + pub fn finish(mut self, check_child_scripts: bool, database: &Database) -> IndexSet { self.pop(); if check_child_scripts { let unused = &self.code_seen - &self.code_used; for expression in unused { let Expression::Code(statements) = expression else { - error!("non-code in unused"); continue; }; // trace!("-- Checking external scope"); - let mut external_scope = Self::create(&self.ignored_vars, true); + let mut external_scope = + Self::create(self.global.clone(), &self.ignored_vars, true); external_scope.eval_statements(&statements, database); self.errors .extend(external_scope.finish(check_child_scripts, database)); } } - self.errors.clone() + self.errors } pub fn push(&mut self) { @@ -136,14 +136,15 @@ impl SciptScope { &mut self, var: &str, local: bool, - possible_values: HashSet, + possible_values: IndexSet, source: VarSource, ) { - trace!("var_assign: {} @ {}", var, self.local.len()); + // println!("var_assign: `{var}` local:{local} lvl: {}", self.local.len()); let var_lower = var.to_ascii_lowercase(); if !var_lower.starts_with('_') { - let holder = self.global.entry(var_lower).or_insert(VarHolder { - possible: HashSet::new(), + let mut global_m = self.global.borrow_mut(); + let holder = global_m.entry(var_lower).or_insert(VarHolder { + possible: IndexSet::new(), usage: 0, source, }); @@ -178,7 +179,7 @@ impl SciptScope { let holder = self.local[stack_level] .entry(var_lower) .or_insert(VarHolder { - possible: HashSet::new(), + possible: IndexSet::new(), usage: 0, source, }); @@ -192,8 +193,9 @@ impl SciptScope { var: &str, source: &Range, peek: bool, - ) -> HashSet { + ) -> IndexSet { let var_lower = var.to_ascii_lowercase(); + let mut global_m = self.global.borrow_mut(); let holder_option = if var_lower.starts_with('_') { let stack_level_search = self .local @@ -213,14 +215,14 @@ impl SciptScope { stack_level -= stack_level_search.expect("is_some"); }; self.local[stack_level].get_mut(&var_lower) - } else if self.global.contains_key(&var_lower) { - self.global.get_mut(&var_lower) + } else if global_m.contains_key(&var_lower) { + global_m.get_mut(&var_lower) } else { - return HashSet::from([GameValue::Anything]); + return IndexSet::from([GameValue::Anything]); }; if holder_option.is_none() { // we've reported the error above, just return Any so it doesn't fail everything after - HashSet::from([GameValue::Anything]) + IndexSet::from([GameValue::Anything]) } else { let holder = holder_option.expect("is_some"); holder.usage += 1; @@ -231,6 +233,7 @@ impl SciptScope { // Assume that a ignored global var could be anything set.insert(GameValue::Anything); } + // println!("var_retrieve: `{var}` {set:?}"); set } } @@ -242,39 +245,41 @@ impl SciptScope { &mut self, expression: &Expression, database: &Database, - ) -> HashSet { + ) -> IndexSet { let mut debug_type = String::new(); let possible_values = match expression { Expression::Variable(var, source) => self.var_retrieve(var, source, false), - Expression::Number(..) => HashSet::from([GameValue::Number(Some(expression.clone()))]), + Expression::Number(..) => IndexSet::from([GameValue::Number(Some(expression.clone()))]), Expression::Boolean(..) => { - HashSet::from([GameValue::Boolean(Some(expression.clone()))]) + IndexSet::from([GameValue::Boolean(Some(expression.clone()))]) } - Expression::String(..) => HashSet::from([GameValue::String(Some(expression.clone()))]), + Expression::String(..) => IndexSet::from([GameValue::String(Some(expression.clone()))]), Expression::Array(array, _) => { let gv_array: Vec> = array .iter() .map(|e| self.eval_expression(e, database).into_iter().collect()) .collect(); - HashSet::from([GameValue::Array(Some(gv_array), None)]) + IndexSet::from([GameValue::Array(Some(gv_array), None)]) } Expression::NularCommand(cmd, source) => { debug_type = format!("[N:{}]", cmd.as_str()); - let cmd_set = GameValue::from_cmd(expression, None, None, database); + let mut cmd_set = GameValue::from_cmd(expression, None, None, database); if cmd_set.is_empty() { // is this possible? self.errors .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + cmd_set.insert(GameValue::Anything); // don't cause confusing errors for code downstream } cmd_set } Expression::UnaryCommand(cmd, rhs, source) => { debug_type = format!("[U:{}]", cmd.as_str()); let rhs_set = self.eval_expression(rhs, database); - let cmd_set = GameValue::from_cmd(expression, None, Some(&rhs_set), database); + let mut cmd_set = GameValue::from_cmd(expression, None, Some(&rhs_set), database); if cmd_set.is_empty() { self.errors .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + cmd_set.insert(GameValue::Anything); // don't cause confusing errors for code downstream } let return_set = match cmd { UnaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { @@ -299,12 +304,13 @@ impl SciptScope { debug_type = format!("[B:{}]", cmd.as_str()); let lhs_set = self.eval_expression(lhs, database); let rhs_set = self.eval_expression(rhs, database); - let cmd_set = + let mut cmd_set = GameValue::from_cmd(expression, Some(&lhs_set), Some(&rhs_set), database); if cmd_set.is_empty() { // we must have invalid args self.errors .insert(Issue::InvalidArgs(debug_type.clone(), source.clone())); + cmd_set.insert(GameValue::Anything); // don't cause confusing errors for code downstream } let return_set = match cmd { BinaryCommand::Associate => { @@ -318,6 +324,11 @@ impl SciptScope { } BinaryCommand::Else => Some(self.cmd_b_else(&lhs_set, &rhs_set)), BinaryCommand::Named(named) => match named.to_ascii_lowercase().as_str() { + "set" | "pushback" | "pushbackunique" | "append" => { + // these commands modify the LHS by lvalue, assume it is now a generic array + self.cmd_generic_modify_lvalue(lhs); + None + } "params" => Some(self.cmd_generic_params(&rhs_set)), "call" => { self.external_function(&lhs_set, rhs, database); @@ -378,7 +389,7 @@ impl SciptScope { Expression::Code(statements) => { self.code_seen.insert(expression.clone()); debug_type = format!("CODE:{}", statements.content().len()); - HashSet::from([GameValue::Code(Some(expression.clone()))]) + IndexSet::from([GameValue::Code(Some(expression.clone()))]) } Expression::ConsumeableArray(_, _) => unreachable!(""), }; @@ -395,7 +406,7 @@ impl SciptScope { /// Evaluate statements in the current scope fn eval_statements(&mut self, statements: &Statements, database: &Database) { - // let mut return_value = HashSet::new(); + // let mut return_value = IndexSet::new(); for statement in statements.content() { match statement { Statement::AssignGlobal(var, expression, source) => { @@ -437,7 +448,7 @@ pub fn run_processed( processed: &Processed, database: &Database, ) -> Vec { - let mut ignored_vars = HashSet::new(); + let mut ignored_vars = IndexSet::new(); ignored_vars.insert("_this".to_string()); let Ok(re1) = Regex::new(r"\/\/ ?IGNORE_PRIVATE_WARNING ?\[(.*)\]") else { return Vec::new(); @@ -453,7 +464,32 @@ pub fn run_processed( } } - let mut scope = SciptScope::create(&ignored_vars, false); + let global = Rc::new(RefCell::new(Stack::new())); + let mut scope = SciptScope::create(global, &ignored_vars, false); scope.eval_statements(statements, database); - scope.finish(true, database).into_iter().collect() + let rv: Vec = scope.finish(true, database).into_iter().collect(); + // for ig in ignored_vars.clone() { + // if ig == "_this" { + // continue; + // } + // let mut igtest = ignored_vars.clone(); + // igtest.remove(&ig); + + // let global = Rc::new(RefCell::new(Stack::new())); + // let mut scope = SciptScope::create(global, &igtest, false); + // scope.eval_statements(statements, database); + // let path = processed.sources(); + // let new = scope.finish(true, database).len(); + // if new <= rv.len() { + // println!( + // "in {:?}-{:?} and {} is undeeded [{}->{}]", + // path[0].0, + // path[path.len() - 1].0, + // ig, + // rv.len(), + // new + // ); + // } + // } + rv } From 21968e381088840425e06b74a2b36ede11092167 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 26 Feb 2025 16:18:35 -0600 Subject: [PATCH 22/24] fix merge --- Cargo.lock | 253 ++++++++++-------- .../lints__simple_s06_find_in_str.snap | 4 +- 2 files changed, 137 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcebc2c41..7866f76e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,11 +147,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -183,9 +184,9 @@ dependencies = [ [[package]] name = "arma-rs" -version = "1.11.10" +version = "1.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e722ed20b6266ceccf4334d32a528fcef9a2dca341b8bbcceb99eb7152081239" +checksum = "5e0342de1990248c43109236de498e358837c95199f8136193b990b40e3e0461" dependencies = [ "arma-rs-proc", "crossbeam-channel", @@ -200,9 +201,9 @@ dependencies = [ [[package]] name = "arma-rs-proc" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936ed035ff4e775bd50ff94ccdb44f236d1ca012c376347b048fb6b9861833b7" +checksum = "bf67c0d0c7a59275e5ac4f3fce0cbdbcf3ba12e47bc30be6a3327d6a1bc151f8" dependencies = [ "proc-macro2", "quote", @@ -250,9 +251,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" +checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" dependencies = [ "proc-macro2", "quote", @@ -346,9 +347,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitstream-io" @@ -376,9 +377,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata 0.4.9", @@ -405,9 +406,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -450,9 +451,9 @@ dependencies = [ [[package]] name = "casey" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614586263949597dcc18675da12ef9b429135e13628d92eb8b8c6fa50ca5656b" +checksum = "8e779867f62d81627d1438e0d3fb6ed7d7c9d64293ca6d87a1e88781b94ece1c" dependencies = [ "syn", ] @@ -498,9 +499,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -640,15 +641,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width 0.1.14", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] @@ -780,18 +781,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -808,9 +809,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1097,9 +1098,9 @@ dependencies = [ [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" @@ -1112,9 +1113,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1122,9 +1123,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1166,15 +1167,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -1416,7 +1417,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "libgit2-sys", "log", @@ -1498,13 +1499,14 @@ version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d752747ddabc4c1a70dd28e72f2e3c218a816773e0d7faf67433f1acfa6cba7c" dependencies = [ + "derive_builder", "log", "num-order", "pest", "pest_derive", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.11", ] [[package]] @@ -1782,6 +1784,7 @@ dependencies = [ "hemtt-lzo", "hemtt-preprocessor", "hemtt-workspace", + "indexmap", "insta", "linkme", "paste", @@ -1855,11 +1858,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1952,9 +1955,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1996,9 +1999,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", @@ -2187,6 +2190,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -2249,9 +2258,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" dependencies = [ "byteorder-lite", "quick-error 2.0.1", @@ -2447,9 +2456,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.74" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -2563,7 +2572,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -2584,9 +2593,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.20" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" dependencies = [ "cc", "libc", @@ -2628,9 +2637,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -2656,9 +2665,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "loom" @@ -3096,7 +3105,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "libc", "objc2", @@ -3104,9 +3113,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -3144,7 +3153,7 @@ version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -3318,18 +3327,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -3337,9 +3346,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -3349,7 +3358,7 @@ dependencies = [ name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] @@ -3376,9 +3385,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3443,9 +3452,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -3505,9 +3514,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -3546,7 +3555,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "memchr", "pulldown-cmark-escape", "unicase", @@ -3581,9 +3590,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", "serde", @@ -3591,9 +3600,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -3706,11 +3715,11 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -3837,7 +3846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" dependencies = [ "ahash", - "bitflags 2.6.0", + "bitflags 2.8.0", "instant", "num-traits", "once_cell", @@ -3949,11 +3958,11 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3997,9 +4006,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rusty-forkfork" @@ -4077,7 +4086,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4086,9 +4095,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -4286,6 +4295,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -4494,7 +4509,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4778,12 +4793,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] @@ -4801,9 +4815,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5071,9 +5085,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -5230,7 +5244,7 @@ dependencies = [ "futures-util", "headers", "http 0.2.12", - "hyper 0.14.31", + "hyper 0.14.32", "log", "mime", "mime_guess", @@ -5270,24 +5284,24 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.97" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.97" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -5296,9 +5310,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.47" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -5309,9 +5323,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.97" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5319,9 +5333,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.97" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -5332,15 +5346,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.97" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.74" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -5986,9 +6003,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap b/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap index cc5fa6f69..635cb5f04 100644 --- a/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap +++ b/libs/sqf/tests/snapshots/lints__simple_s06_find_in_str.snap @@ -14,7 +14,7 @@ expression: "lint(stringify! (s06_find_in_str), true)" warning[L-S06]: string search using `in` is faster than `find` ┌─ s06_find_in_str.sqf:2:19 │ -2 │ private _hasBar = _things find "bar" > -1; - │ ^^^^^^^^^^^^^^^^^^^^^^^ using `find` with -1 +2 │ private _hasBar = things find "bar" > -1; + │ ^^^^^^^^^^^^^^^^^^^^^^ using `find` with -1 │ = try: "bar" in things From 56cf7a4e2e39549a754be4b28833082354d42351 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Wed, 26 Feb 2025 16:19:33 -0600 Subject: [PATCH 23/24] fix merge --- libs/sqf/tests/snapshots/optimizer__simple_chain.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/sqf/tests/snapshots/optimizer__simple_chain.snap b/libs/sqf/tests/snapshots/optimizer__simple_chain.snap index ec9f5ffd1..8f083a0ef 100644 --- a/libs/sqf/tests/snapshots/optimizer__simple_chain.snap +++ b/libs/sqf/tests/snapshots/optimizer__simple_chain.snap @@ -241,4 +241,5 @@ Statements { ], source: "\naa * 3 * 5; \nbb / 3 * 5; \ncc * 3 / 5; \ndd / 3 / 5; \n\nee * 2 * 3 / 4 * 5;\n3 * 4 * ff / 5 / 6;\n\ngg * 1 / 0;\n\n\n\nmm + 20 + 30;\nnn + 20 - 30;\noo - 20 + 30;\npp - 20 - 30;\n\nzz * 6 - 7;\n", span: 1..178, + issues: [], } From 7d409ff2627ed32f48f3ce8b9758651bb551cc29 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Thu, 27 Feb 2025 12:10:19 -0600 Subject: [PATCH 24/24] update tests --- libs/sqf/tests/snapshots/simple__simple_eventhandler.snap | 8 ++++++++ libs/sqf/tests/snapshots/simple__simple_foreach.snap | 3 +++ .../tests/snapshots/simple__simple_get_visibility.snap | 2 ++ libs/sqf/tests/snapshots/simple__simple_include.snap | 1 + libs/sqf/tests/snapshots/simple__simple_semicolons.snap | 1 + 5 files changed, 15 insertions(+) diff --git a/libs/sqf/tests/snapshots/simple__simple_eventhandler.snap b/libs/sqf/tests/snapshots/simple__simple_eventhandler.snap index e09ece6fb..09537998d 100644 --- a/libs/sqf/tests/snapshots/simple__simple_eventhandler.snap +++ b/libs/sqf/tests/snapshots/simple__simple_eventhandler.snap @@ -27,6 +27,7 @@ expression: ast ], source: "deleteVehicle _x", span: 3..19, + issues: [], }, ), NularCommand( @@ -110,6 +111,7 @@ expression: ast ], source: "alive _x", span: 115..123, + issues: [], }, ), 106..112, @@ -140,6 +142,7 @@ expression: ast ], source: "deleteVehicle _x;", span: 149..166, + issues: [], }, ), NularCommand( @@ -155,6 +158,7 @@ expression: ast ], source: "allPlayers findIf { alive _x };\n {\n deleteVehicle _x;\n } forEach allPlayers;", span: 95..196, + issues: [], }, ), 80..84, @@ -164,6 +168,7 @@ expression: ast ], source: "if (alive player) then {\n allPlayers findIf { alive _x };\n {\n deleteVehicle _x;\n } forEach allPlayers;\n };", span: 62..203, + issues: [], }, ), ], @@ -242,6 +247,7 @@ expression: ast ], source: "deleteVehicle _x", span: 294..310, + issues: [], }, ), NularCommand( @@ -257,6 +263,7 @@ expression: ast ], source: "{ deleteVehicle _x } count allPlayers;", span: 292..330, + issues: [], }, ), 277..281, @@ -266,6 +273,7 @@ expression: ast ], source: "if (alive player) then {\n { deleteVehicle _x } count allPlayers;\n };", span: 259..337, + issues: [], }, ), ], diff --git a/libs/sqf/tests/snapshots/simple__simple_foreach.snap b/libs/sqf/tests/snapshots/simple__simple_foreach.snap index 24bc3e967..96cab0241 100644 --- a/libs/sqf/tests/snapshots/simple__simple_foreach.snap +++ b/libs/sqf/tests/snapshots/simple__simple_foreach.snap @@ -27,6 +27,7 @@ expression: ast ], source: "deleteVehicle _x;", span: 7..24, + issues: [], }, ), NularCommand( @@ -106,6 +107,7 @@ expression: ast ], source: "_x setDamage 1;", span: 97..112, + issues: [], }, ), UnaryCommand( @@ -125,6 +127,7 @@ expression: ast ], source: "systemChat format [\"%1\", _x];\n {\n _x setDamage 1;\n } forEach crew _x;", span: 53..135, + issues: [], }, ), NularCommand( diff --git a/libs/sqf/tests/snapshots/simple__simple_get_visibility.snap b/libs/sqf/tests/snapshots/simple__simple_get_visibility.snap index 0e168ddfe..3c935a5e2 100644 --- a/libs/sqf/tests/snapshots/simple__simple_get_visibility.snap +++ b/libs/sqf/tests/snapshots/simple__simple_get_visibility.snap @@ -86,6 +86,7 @@ expression: ast ], source: "_arg1 = [eyePos _arg1, _arg1]", span: 66..95, + issues: [], }, ), 59..63, @@ -151,6 +152,7 @@ expression: ast ], source: "_arg2 = [eyePos _arg2, _arg2]", span: 138..167, + issues: [], }, ), 131..135, diff --git a/libs/sqf/tests/snapshots/simple__simple_include.snap b/libs/sqf/tests/snapshots/simple__simple_include.snap index 726ce909b..1adfd6f81 100644 --- a/libs/sqf/tests/snapshots/simple__simple_include.snap +++ b/libs/sqf/tests/snapshots/simple__simple_include.snap @@ -130,6 +130,7 @@ expression: ast ], source: "private thinghi = _x + \"test\";", span: 94..124, + issues: [], }, ), Array( diff --git a/libs/sqf/tests/snapshots/simple__simple_semicolons.snap b/libs/sqf/tests/snapshots/simple__simple_semicolons.snap index ede822834..a982af3d4 100644 --- a/libs/sqf/tests/snapshots/simple__simple_semicolons.snap +++ b/libs/sqf/tests/snapshots/simple__simple_semicolons.snap @@ -78,6 +78,7 @@ expression: ast ], source: "systemChat \"this is a test\";", span: 153..181, + issues: [], }, ), 136..140,