From b33d492da3bcc6fcf2db2a6278d1ee2f34f5838f Mon Sep 17 00:00:00 2001 From: BrettMayson Date: Mon, 10 Jun 2024 16:45:50 -0600 Subject: [PATCH] sqf: event handlers (#702) --- Cargo.lock | 5 +- Cargo.toml | 2 +- bin/Cargo.toml | 1 + bin/src/commands/build.rs | 2 +- bin/src/commands/dev.rs | 2 +- .../error/bcle6_launch_config_not_found.rs | 9 +- bin/src/commands/mod.rs | 1 + bin/src/commands/wiki.rs | 27 +++ bin/src/lib.rs | 4 + bin/src/modules/sqf.rs | 114 +++++++++++- hls/build.rs | 2 +- hls/src/sqf/mod.rs | 2 +- libs/common/src/lib.rs | 1 + .../codes/pe9_function_call_argument_count.rs | 2 +- libs/sqf/src/analyze/codes/mod.rs | 6 +- ...ion.rs => sae1_require_version_command.rs} | 21 ++- .../codes/sae2_require_version_event.rs | 90 +++++++++ .../src/analyze/codes/saw1_unknown_event.rs | 106 +++++++++++ .../analyze/codes/saw2_wrong_event_command.rs | 126 +++++++++++++ libs/sqf/src/analyze/event_handlers.rs | 176 ++++++++++++++++++ libs/sqf/src/analyze/if_assign.rs | 22 +-- libs/sqf/src/analyze/mod.rs | 28 ++- libs/sqf/src/analyze/required_version.rs | 2 +- libs/sqf/src/parser/database/mod.rs | 24 ++- libs/sqf/tests/analyze.rs | 2 +- libs/sqf/tests/errors.rs | 2 +- libs/sqf/tests/preprocessor.rs | 2 +- libs/sqf/tests/simple.rs | 2 +- 28 files changed, 718 insertions(+), 65 deletions(-) create mode 100644 bin/src/commands/wiki.rs rename libs/sqf/src/analyze/codes/{sae1_require_version.rs => sae1_require_version_command.rs} (72%) create mode 100644 libs/sqf/src/analyze/codes/sae2_require_version_event.rs create mode 100644 libs/sqf/src/analyze/codes/saw1_unknown_event.rs create mode 100644 libs/sqf/src/analyze/codes/saw2_wrong_event_command.rs create mode 100644 libs/sqf/src/analyze/event_handlers.rs diff --git a/Cargo.lock b/Cargo.lock index f01b5971..b0de1539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "arma3-wiki" -version = "0.1.18" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1577db41a42995e127b93852bebe577c1be041483bc756efadcc23b95ff31c57" +checksum = "c92dc3d309c6d0b964a2c7e1a153a3b5301ca343dd650550e8b3b05aefde5f94" dependencies = [ "directories", "fs_extra", @@ -1083,6 +1083,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "hemtt" version = "1.12.3" dependencies = [ + "arma3-wiki", "clap", "dialoguer", "enable-ansi-support", diff --git a/Cargo.toml b/Cargo.toml index 24885801..74635fa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ future_incompatible = "warn" nonstandard_style = "warn" [workspace.dependencies] -arma3-wiki = "0.1.18" +arma3-wiki = "0.2.4" byteorder = "1.5.0" chumsky = "0.9.3" clap = "4.5.4" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index da77896d..46d03b7e 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -29,6 +29,7 @@ hemtt-signing = { path = "../libs/signing", version = "1.0.0" } hemtt-sqf = { path = "../libs/sqf", version = "1.0.0" } hemtt-workspace = { path = "../libs/workspace", version = "1.0.0" } +arma3-wiki = { workspace = true } clap = { workspace = true } dialoguer = "0.11.0" fs_extra = "1.3.0" diff --git a/bin/src/commands/build.rs b/bin/src/commands/build.rs index 88e8f968..88a298db 100644 --- a/bin/src/commands/build.rs +++ b/bin/src/commands/build.rs @@ -100,7 +100,7 @@ pub fn executor(ctx: Context, matches: &ArgMatches) -> Executor { if matches.get_one::("no-rap") != Some(&true) { executor.add_module(Box::::default()); } - executor.add_module(Box::new(SQFCompiler { compile: expsqfc })); + executor.add_module(Box::new(SQFCompiler::new(expsqfc))); #[cfg(not(target_os = "macos"))] if !expsqfc { executor.add_module(Box::::default()); diff --git a/bin/src/commands/dev.rs b/bin/src/commands/dev.rs index 48240d9d..c7e59da5 100644 --- a/bin/src/commands/dev.rs +++ b/bin/src/commands/dev.rs @@ -127,7 +127,7 @@ pub fn context(matches: &ArgMatches, launch_optionals: &[String]) -> Result::default()); executor.add_module(Box::::default()); - executor.add_module(Box::new(SQFCompiler { compile: expsqfc })); + executor.add_module(Box::new(SQFCompiler::new(expsqfc))); #[cfg(not(target_os = "macos"))] if !expsqfc { executor.add_module(Box::::default()); diff --git a/bin/src/commands/launch/error/bcle6_launch_config_not_found.rs b/bin/src/commands/launch/error/bcle6_launch_config_not_found.rs index bccb4ab5..6764e91c 100644 --- a/bin/src/commands/launch/error/bcle6_launch_config_not_found.rs +++ b/bin/src/commands/launch/error/bcle6_launch_config_not_found.rs @@ -21,14 +21,7 @@ impl Code for LaunchConfigNotFound { if self.similar.is_empty() { None } else { - Some(format!( - "did you mean `{}`?", - self.similar - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .join("`, `") - )) + Some(format!("did you mean `{}`?", self.similar.join("`, `"))) } } diff --git a/bin/src/commands/mod.rs b/bin/src/commands/mod.rs index cd98739b..c4054d85 100644 --- a/bin/src/commands/mod.rs +++ b/bin/src/commands/mod.rs @@ -7,3 +7,4 @@ pub mod release; pub mod script; pub mod utils; pub mod value; +pub mod wiki; diff --git a/bin/src/commands/wiki.rs b/bin/src/commands/wiki.rs new file mode 100644 index 00000000..4b2c5616 --- /dev/null +++ b/bin/src/commands/wiki.rs @@ -0,0 +1,27 @@ +use clap::{ArgMatches, Command}; +use hemtt_sqf::parser::database::Database; + +use crate::{error::Error, report::Report}; + +#[must_use] +pub fn cli() -> Command { + Command::new("wiki") + .about("Manage the Arma 3 wiki") + .subcommand( + Command::new("force-pull") + .about("Force pull the wiki, if updates you need have been pushed very recently"), + ) +} + +/// Execute the wiki command +/// +/// # Errors +/// [`Error`] depending on the modules +/// +/// # Panics +/// If a name is not provided, but this is usually handled by clap +pub fn execute(_matches: &ArgMatches) -> Result { + // TODO right now just assumes force-pull since that's the only subcommand + let _ = Database::empty(true); + Ok(Report::new()) +} diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 03aede7f..bc140b01 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -32,6 +32,7 @@ pub fn cli() -> Command { .subcommand(commands::script::cli()) .subcommand(commands::utils::cli()) .subcommand(commands::value::cli()) + .subcommand(commands::wiki::cli()) .arg( clap::Arg::new("threads") .global(true) @@ -139,6 +140,9 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { Some(("value", matches)) => commands::value::execute(matches) .map_err(std::convert::Into::into) .map(Some), + Some(("wiki", matches)) => commands::wiki::execute(matches) + .map_err(std::convert::Into::into) + .map(Some), _ => unreachable!(), }; if let Some(report) = report? { diff --git a/bin/src/modules/sqf.rs b/bin/src/modules/sqf.rs index 9347c3cc..66274caf 100644 --- a/bin/src/modules/sqf.rs +++ b/bin/src/modules/sqf.rs @@ -1,10 +1,15 @@ -use std::sync::atomic::{AtomicU16, Ordering}; +use std::sync::{ + atomic::{AtomicU16, Ordering}, + Arc, +}; +use hemtt_common::version::Version; use hemtt_preprocessor::Processor; use hemtt_sqf::{ analyze::analyze, parser::{database::Database, ParserError}, }; +use hemtt_workspace::reporting::{Code, Diagnostic, Severity}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use crate::{context::Context, error::Error, report::Report}; @@ -14,6 +19,17 @@ use super::Module; #[derive(Default)] pub struct SQFCompiler { pub compile: bool, + pub database: Option, +} + +impl SQFCompiler { + #[must_use] + pub const fn new(compile: bool) -> Self { + Self { + compile, + database: None, + } + } } impl Module for SQFCompiler { @@ -21,6 +37,11 @@ impl Module for SQFCompiler { "SQF" } + fn init(&mut self, ctx: &Context) -> Result { + self.database = Some(Database::a3_with_workspace(ctx.workspace_path(), false)?); + Ok(Report::new()) + } + #[allow(clippy::too_many_lines)] fn pre_build(&self, ctx: &Context) -> Result { let mut report = Report::new(); @@ -37,7 +58,7 @@ impl Module for SQFCompiler { } } } - let database = Database::a3_with_workspace(ctx.workspace_path())?; + let database = self.database.as_ref().expect("database not initialized"); let reports = entries .par_iter() .map(|(addon, entry)| { @@ -47,10 +68,10 @@ impl Module for SQFCompiler { for warning in processed.warnings() { report.warn(warning.clone()); } - match hemtt_sqf::parser::run(&database, &processed) { + match hemtt_sqf::parser::run(database, &processed) { Ok(sqf) => { let (warnings, errors) = - analyze(&sqf, Some(ctx.config()), &processed, Some(addon), &database); + analyze(&sqf, Some(ctx.config()), &processed, Some(addon), database); for warning in warnings { report.warn(warning); } @@ -105,4 +126,89 @@ impl Module for SQFCompiler { ); Ok(report) } + + fn post_build(&self, ctx: &Context) -> Result { + let mut report = Report::new(); + let mut required_version = Version::new(0, 0, 0, None); + let mut required_by = Vec::new(); + for addon in ctx.addons() { + let addon_version = addon.build_data().required_version(); + if let Some((version, _, _)) = addon_version { + if version > required_version { + required_version = version; + required_by = vec![addon.name().to_string()]; + } else if version == required_version { + required_by.push(addon.name().to_string()); + } + } + } + + let database = self.database.as_ref().expect("database not initialized"); + + let wiki_version = arma3_wiki::model::Version::new( + u8::try_from(required_version.major()).unwrap_or_default(), + u8::try_from(required_version.minor()).unwrap_or_default(), + ); + if database.wiki().version() < &wiki_version { + report.warn(Arc::new(RequiresFutureVersion::new( + wiki_version, + required_by, + *database.wiki().version(), + ))); + } + + Ok(report) + } +} + +pub struct RequiresFutureVersion { + required_version: arma3_wiki::model::Version, + required_by: Vec, + wiki_version: arma3_wiki::model::Version, +} +impl Code for RequiresFutureVersion { + fn ident(&self) -> &'static str { + "BSW1" + } + + fn severity(&self) -> Severity { + Severity::Warning + } + + fn message(&self) -> String { + format!( + "Required version `{}` is higher than the current stable `{}`", + self.required_version, self.wiki_version + ) + } + + fn note(&self) -> Option { + Some(format!( + "addons requiring version `{}`: {}", + self.required_version, + self.required_by.join(", ") + )) + } + + fn help(&self) -> Option { + Some("Learn about the `development` branch at `https://community.bistudio.com/wiki/Arma_3:_Steam_Branches`".to_string()) + } + + fn diagnostic(&self) -> Option { + Some(Diagnostic::simple(self)) + } +} + +impl RequiresFutureVersion { + pub fn new( + required_version: arma3_wiki::model::Version, + required_by: Vec, + wiki_version: arma3_wiki::model::Version, + ) -> Self { + Self { + required_version, + required_by, + wiki_version, + } + } } diff --git a/hls/build.rs b/hls/build.rs index 678028b4..c994034b 100644 --- a/hls/build.rs +++ b/hls/build.rs @@ -1,7 +1,7 @@ use arma3_wiki::Wiki; fn main() { - let wiki = Wiki::load(); + let wiki = Wiki::load(true); let mut flow = Vec::with_capacity(500); let mut commands = Vec::with_capacity(3000); diff --git a/hls/src/sqf/mod.rs b/hls/src/sqf/mod.rs index 61d11691..c280280c 100644 --- a/hls/src/sqf/mod.rs +++ b/hls/src/sqf/mod.rs @@ -67,7 +67,7 @@ impl SqfCache { return; } }; - let database = match Database::a3_with_workspace(workspace.root()) { + let database = match Database::a3_with_workspace(workspace.root(), false) { Ok(database) => database, Err(e) => { error!("Failed to create database {:?}", e); diff --git a/libs/common/src/lib.rs b/libs/common/src/lib.rs index 3e1945f0..dc53ede2 100644 --- a/libs/common/src/lib.rs +++ b/libs/common/src/lib.rs @@ -14,6 +14,7 @@ mod sign_version; pub use sign_version::BISignVersion; #[must_use] +/// Returns up to 3 similar values from a haystack. pub fn similar_values<'a>(search: &str, haystack: &'a [&str]) -> Vec<&'a str> { let mut similar = haystack .iter() diff --git a/libs/preprocessor/src/codes/pe9_function_call_argument_count.rs b/libs/preprocessor/src/codes/pe9_function_call_argument_count.rs index 83dc4b25..d5bd22c8 100644 --- a/libs/preprocessor/src/codes/pe9_function_call_argument_count.rs +++ b/libs/preprocessor/src/codes/pe9_function_call_argument_count.rs @@ -45,7 +45,7 @@ impl Code for FunctionCallArgumentCount { None } else { Some(format!( - "did you mean `{}`", + "did you mean `{}`?", self.similar .iter() .map(std::string::ToString::to_string) diff --git a/libs/sqf/src/analyze/codes/mod.rs b/libs/sqf/src/analyze/codes/mod.rs index ed59cd6f..80e0e6b0 100644 --- a/libs/sqf/src/analyze/codes/mod.rs +++ b/libs/sqf/src/analyze/codes/mod.rs @@ -1,4 +1,8 @@ -pub mod sae1_require_version; +pub mod sae1_require_version_command; +pub mod sae2_require_version_event; + +pub mod saw1_unknown_event; +pub mod saw2_wrong_event_command; pub mod saa1_if_assign; pub mod saa2_find_in_str; diff --git a/libs/sqf/src/analyze/codes/sae1_require_version.rs b/libs/sqf/src/analyze/codes/sae1_require_version_command.rs similarity index 72% rename from libs/sqf/src/analyze/codes/sae1_require_version.rs rename to libs/sqf/src/analyze/codes/sae1_require_version_command.rs index 40abe2a2..b98c87c4 100644 --- a/libs/sqf/src/analyze/codes/sae1_require_version.rs +++ b/libs/sqf/src/analyze/codes/sae1_require_version_command.rs @@ -6,17 +6,17 @@ use hemtt_workspace::{ WorkspacePath, }; -pub struct InsufficientRequiredVersion { +pub struct InsufficientRequiredVersionCommand { command: String, span: Range, version: Version, - required: (Version, WorkspacePath, Range), + required: (Option, WorkspacePath, Range), stable: Version, diagnostic: Option, } -impl Code for InsufficientRequiredVersion { +impl Code for InsufficientRequiredVersionCommand { fn ident(&self) -> &'static str { "SAE1" } @@ -48,7 +48,7 @@ impl Code for InsufficientRequiredVersion { } } -impl InsufficientRequiredVersion { +impl InsufficientRequiredVersionCommand { #[must_use] pub fn new( command: String, @@ -62,7 +62,13 @@ impl InsufficientRequiredVersion { command, span, version, - required, + required: { + if required.0.major() == 0 && required.0.minor() == 0 { + (None, required.1, required.2) + } else { + (Some(required.0), required.1, required.2) + } + }, stable, diagnostic: None, @@ -76,7 +82,10 @@ impl InsufficientRequiredVersion { }; self.diagnostic = Some(diag.with_label( Label::secondary(self.required.1.clone(), self.required.2.clone()).with_message( - format!("CfgPatch only requires version {}", self.required.0), + self.required.0.map_or_else( + || "CfgPatch doesn't specify `requiredVersion`".to_string(), + |required| format!("CfgPatch requires version {required}"), + ), ), )); self diff --git a/libs/sqf/src/analyze/codes/sae2_require_version_event.rs b/libs/sqf/src/analyze/codes/sae2_require_version_event.rs new file mode 100644 index 00000000..a89b0375 --- /dev/null +++ b/libs/sqf/src/analyze/codes/sae2_require_version_event.rs @@ -0,0 +1,90 @@ +use std::ops::Range; + +use arma3_wiki::model::Version; +use hemtt_workspace::{ + reporting::{Code, Diagnostic, Label, Processed}, + WorkspacePath, +}; + +pub struct InsufficientRequiredVersionEvent { + event: String, + span: Range, + version: Version, + required: (Option, WorkspacePath, Range), + stable: Version, + + diagnostic: Option, +} + +impl Code for InsufficientRequiredVersionEvent { + fn ident(&self) -> &'static str { + "SAE1" + } + + fn message(&self) -> String { + format!("event `{}` requires version {}", self.event, self.version) + } + + fn label_message(&self) -> String { + format!("requires version {}", self.version) + } + + fn note(&self) -> Option { + if self.version > self.stable { + Some(format!( + "Current stable version is {}. Using {} will require the development branch.", + self.stable, self.version + )) + } else { + None + } + } + + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl InsufficientRequiredVersionEvent { + #[must_use] + pub fn new( + event: String, + span: Range, + version: Version, + required: (Version, WorkspacePath, Range), + stable: Version, + processed: &Processed, + ) -> Self { + Self { + event, + span, + version, + required: { + if required.0.major() == 0 && required.0.minor() == 0 { + (None, required.1, required.2) + } else { + (Some(required.0), required.1, required.2) + } + }, + stable, + + diagnostic: None, + } + .generate_processed(processed) + } + + fn generate_processed(mut self, processed: &Processed) -> Self { + let Some(diag) = Diagnostic::new_for_processed(&self, self.span.clone(), processed) else { + return self; + }; + self.diagnostic = Some(diag.with_label( + Label::secondary(self.required.1.clone(), self.required.2.clone()).with_message( + self.required.0.map_or_else( + || "CfgPatch doesn't specify `requiredVersion`".to_string(), + |required| format!("CfgPatch requires version {required}"), + ), + ), + )); + self + } +} diff --git a/libs/sqf/src/analyze/codes/saw1_unknown_event.rs b/libs/sqf/src/analyze/codes/saw1_unknown_event.rs new file mode 100644 index 00000000..a3e562a3 --- /dev/null +++ b/libs/sqf/src/analyze/codes/saw1_unknown_event.rs @@ -0,0 +1,106 @@ +use std::{ops::Range, sync::Arc}; + +use arma3_wiki::model::EventHandlerNamespace; +use hemtt_common::similar_values; +use hemtt_workspace::reporting::{Code, Diagnostic, Processed, Severity}; + +use crate::parser::database::Database; + +pub struct UnknownEvent { + span: Range, + command: String, + id: Arc, + + similar: Vec, + + diagnostic: Option, +} + +impl Code for UnknownEvent { + fn ident(&self) -> &'static str { + "SAW1" + } + + fn severity(&self) -> Severity { + if self.id.to_lowercase() == "damaged" { + Severity::Error + } else { + Severity::Warning + } + } + + fn message(&self) -> String { + format!("Using `{}` with unknown event `{}`", self.command, self.id) + } + + fn label_message(&self) -> String { + format!("unknown event `{}`", self.id) + } + + fn help(&self) -> Option { + if self.similar.is_empty() { + None + } else { + Some(format!("Did you mean: `{}`?", self.similar.join("`, `"))) + } + } + + // fn suggestion(&self) -> Option { + // Some(format!("\"{}\"", self.constant.0)) + // } + + fn diagnostic(&self) -> Option { + self.diagnostic.clone().map(|d| { + if self.id.to_lowercase() == "damaged" { + d.with_help("Damaged is a common typo for `Dammaged`. An error has been raised to prevent accidental usage.") + } else { + d + } + }) + } +} + +impl UnknownEvent { + #[must_use] + pub fn new( + nss: &[EventHandlerNamespace], + span: Range, + command: String, + id: Arc, + processed: &Processed, + database: &Database, + ) -> Self { + Self { + span, + command, + + similar: { + let mut haystack = Vec::new(); + for (dns, ehs) in database.wiki().event_handlers() { + if !nss.contains(dns) { + continue; + } + for eh in ehs { + haystack.push(eh.id()); + } + } + let mut similar: Vec = similar_values(&id, &haystack) + .into_iter() + .map(std::string::ToString::to_string) + .collect(); + similar.sort(); + similar.dedup(); + similar + }, + + id, + 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/codes/saw2_wrong_event_command.rs b/libs/sqf/src/analyze/codes/saw2_wrong_event_command.rs new file mode 100644 index 00000000..cf781800 --- /dev/null +++ b/libs/sqf/src/analyze/codes/saw2_wrong_event_command.rs @@ -0,0 +1,126 @@ +use std::{ops::Range, sync::Arc}; + +use arma3_wiki::model::EventHandlerNamespace; +use hemtt_workspace::reporting::{Code, Diagnostic, Processed, Severity}; + +use crate::parser::database::Database; + +pub struct WrongEventCommand { + span: Range, + command: String, + id: Arc, + target: Option<(String, bool)>, + + alternatives: Vec<(String, bool)>, + + diagnostic: Option, +} + +impl Code for WrongEventCommand { + fn ident(&self) -> &'static str { + "SAW2" + } + + fn severity(&self) -> Severity { + Severity::Warning + } + + fn message(&self) -> String { + format!( + "Event `{}` was not expected for command `{}`", + self.id, self.command + ) + } + + fn label_message(&self) -> String { + format!("not supported by command `{}`", self.command) + } + + fn suggestion(&self) -> Option { + if self.alternatives.len() == 1 { + if self.alternatives[0].1 { + if let Some((target, _)) = &self.target { + Some(format!( + "{} {} [\"{}\", {{ …", + target, self.alternatives[0].0, self.id + )) + } else { + Some(format!( + "{{target}} {} [\"{}\", {{ …", + self.alternatives[0].0, self.id + )) + } + } else { + Some(self.alternatives[0].0.clone()) + } + } else { + None + } + } + + fn help(&self) -> Option { + if self.alternatives.is_empty() { + None + } else { + Some(format!( + "Did you mean: `{}`?", + self.alternatives + .iter() + .map(|(a, _)| a.as_str()) + .collect::>() + .join("`, `") + )) + } + } + + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl WrongEventCommand { + #[must_use] + pub fn new( + span: Range, + command: String, + id: Arc, + target: Option<(String, bool)>, + namespaces: Vec, + processed: &Processed, + database: &Database, + ) -> Self { + let prefix = command.chars().take(3).collect::(); + Self { + span, + command, + id, + target, + alternatives: { + let mut alternatives = Vec::new(); + for ns in namespaces { + println!("Possible alternatives: {:?}", ns.commands()); + ns.commands() + .iter() + .filter(|c| c.contains(&prefix)) + .for_each(|c| { + alternatives.push(((*c).to_string(), { + database.wiki().commands().get(*c).map_or(false, |c| { + c.syntax().first().map_or(false, |s| s.call().is_binary()) + }) + })); + }); + } + alternatives.sort(); + alternatives.dedup(); + alternatives + }, + 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/event_handlers.rs b/libs/sqf/src/analyze/event_handlers.rs new file mode 100644 index 00000000..b264221a --- /dev/null +++ b/libs/sqf/src/analyze/event_handlers.rs @@ -0,0 +1,176 @@ +use std::{ops::Range, sync::Arc}; + +use arma3_wiki::model::{EventHandlerNamespace, ParsedEventHandler}; +use hemtt_workspace::{ + addons::Addon, + reporting::{Code, Processed}, +}; + +use crate::{ + analyze::{ + codes::{saw1_unknown_event::UnknownEvent, saw2_wrong_event_command::WrongEventCommand}, + extract_constant, + }, + parser::database::Database, + BinaryCommand, Expression, Statements, UnaryCommand, +}; + +use super::{ + codes::sae2_require_version_event::InsufficientRequiredVersionEvent, WarningAndErrors, +}; + +pub fn event_handlers( + addon: Option<&Addon>, + statements: &Statements, + processed: &Processed, + database: &Database, +) -> WarningAndErrors { + let mut warnings: Vec> = Vec::new(); + let mut errors: Vec> = Vec::new(); + for statement in statements.content() { + for expression in statement.walk_expressions() { + let Some((ns, name, id, target)) = get_namespaces(expression) else { + continue; + }; + if ns.is_empty() { + continue; + } + if name.contains("UserAction") { + // Requires arma3-wiki to parse and provide https://community.bistudio.com/wiki/inputAction/actions + continue; + } + let eh = database.wiki().event_handler(&id.0); + warnings.extend(check_unknown( + &ns, + &name, + &id, + target.map(|t| &**t), + &eh, + processed, + database, + )); + errors.extend(check_version( + addon, &ns, &name, &id, &eh, processed, database, + )); + } + } + (warnings, errors) +} + +fn check_unknown( + ns: &[EventHandlerNamespace], + name: &str, + id: &(Arc, &Range), + target: Option<&Expression>, + eh: &[(EventHandlerNamespace, &ParsedEventHandler)], + processed: &Processed, + database: &Database, +) -> Vec> { + if eh.is_empty() { + return vec![Arc::new(UnknownEvent::new( + ns, + id.1.clone(), + name.to_owned(), + id.0.clone(), + processed, + database, + ))]; + } + + if ns.iter().any(|n| eh.iter().any(|(ns, _)| ns == n)) { + return Vec::new(); + } + vec![Arc::new(WrongEventCommand::new( + id.1.clone(), + name.to_owned(), + id.0.clone(), + target.and_then(extract_constant), + eh.iter().map(|(ns, _)| ns).copied().collect(), + processed, + database, + ))] +} + +fn check_version( + addon: Option<&Addon>, + ns: &[EventHandlerNamespace], + name: &str, + id: &(Arc, &Range), + eh: &[(EventHandlerNamespace, &ParsedEventHandler)], + processed: &Processed, + database: &Database, +) -> Vec> { + let Some(addon) = addon else { + return Vec::new(); + }; + let Some(required) = addon.build_data().required_version() else { + // TODO what to do here? + return Vec::new(); + }; + let Some((_, eh)) = eh.iter().find(|(ins, _)| ns.contains(ins)) else { + return Vec::new(); + }; + let Some(since) = eh.since() else { + return Vec::new(); + }; + let Some(version) = since.arma_3() else { + return Vec::new(); + }; + let mut errors: Vec> = Vec::new(); + let wiki_version = arma3_wiki::model::Version::new( + u8::try_from(required.0.major()).unwrap_or_default(), + u8::try_from(required.0.minor()).unwrap_or_default(), + ); + let required = (wiki_version, required.1, required.2); + if wiki_version < *version { + errors.push(Arc::new(InsufficientRequiredVersionEvent::new( + name.to_owned(), + id.1.clone(), + *version, + required, + *database.wiki().version(), + processed, + ))); + } + errors +} + +#[allow(clippy::type_complexity)] +fn get_namespaces( + expression: &Expression, +) -> Option<( + Vec, + String, + (Arc, &Range), + Option<&Box>, +)> { + match expression { + Expression::BinaryCommand(BinaryCommand::Named(name), target, id, _) => Some(( + EventHandlerNamespace::by_command(name), + name.to_owned(), + get_id(id)?, + Some(target), + )), + Expression::UnaryCommand(UnaryCommand::Named(name), id, _) => Some(( + EventHandlerNamespace::by_command(name), + name.to_owned(), + get_id(id)?, + None, + )), + _ => None, + } +} + +fn get_id(expression: &Expression) -> Option<(Arc, &Range)> { + match expression { + Expression::String(id, span, _) => Some((id.clone(), span)), + Expression::Array(items, _) => { + if items.is_empty() { + None + } else { + get_id(&items[0]) + } + } + _ => None, + } +} diff --git a/libs/sqf/src/analyze/if_assign.rs b/libs/sqf/src/analyze/if_assign.rs index dae826dc..d2cdceca 100644 --- a/libs/sqf/src/analyze/if_assign.rs +++ b/libs/sqf/src/analyze/if_assign.rs @@ -7,6 +7,8 @@ use crate::{ UnaryCommand, }; +use super::extract_constant; + pub fn if_assign(statements: &Statements, processed: &Processed) -> Vec> { let mut advice: Vec> = Vec::new(); for statement in statements.content() { @@ -47,23 +49,3 @@ fn check_expression(expression: &Expression, processed: &Processed) -> Vec Option<(String, bool)> { - if let Expression::Code(code) = &expression { - if code.content.len() == 1 { - if let Statement::Expression(expr, _) = &code.content[0] { - return match expr { - Expression::Boolean(bool, _) => Some((bool.to_string(), false)), - Expression::Number(num, _) => Some((num.0.to_string(), false)), - Expression::String(string, _, _) => Some((string.to_string(), true)), - Expression::Variable(var, _) => Some((var.to_string(), false)), - _ => None, - }; - } - } - } - None -} diff --git a/libs/sqf/src/analyze/mod.rs b/libs/sqf/src/analyze/mod.rs index 85519ccf..fc748803 100644 --- a/libs/sqf/src/analyze/mod.rs +++ b/libs/sqf/src/analyze/mod.rs @@ -1,5 +1,6 @@ pub mod codes; +mod event_handlers; mod find_in_str; mod if_assign; mod required_version; @@ -15,9 +16,10 @@ use hemtt_workspace::{ reporting::{Code, Processed}, }; -use crate::{parser::database::Database, Statements}; +use crate::{parser::database::Database, Expression, Statement, Statements}; type Codes = Vec>; +type WarningAndErrors = (Codes, Codes); pub trait Analyze { /// Check if the object is valid and can be rapified @@ -48,9 +50,10 @@ pub fn analyze( addon: Option<&Addon>, database: &Database, ) -> (Codes, Codes) { + let (mut warnings, mut errors) = + event_handlers::event_handlers(addon, statements, processed, database); ( { - let mut warnings = Vec::new(); warnings.extend(if_assign::if_assign(statements, processed)); warnings.extend(find_in_str::find_in_str(statements, processed)); warnings.extend(typename::typename(statements, processed)); @@ -61,7 +64,6 @@ pub fn analyze( warnings }, { - let mut errors = Vec::new(); errors.extend(required_version::required_version( statements, processed, addon, database, )); @@ -69,3 +71,23 @@ pub fn analyze( }, ) } + +/// Extracts a constant from an expression +/// +/// Returns a tuple of the constant and a boolean indicating if quotes are needed +fn extract_constant(expression: &Expression) -> Option<(String, bool)> { + if let Expression::Code(code) = &expression { + if code.content.len() == 1 { + if let Statement::Expression(expr, _) = &code.content[0] { + return match expr { + Expression::Boolean(bool, _) => Some((bool.to_string(), false)), + Expression::Number(num, _) => Some((num.0.to_string(), false)), + Expression::String(string, _, _) => Some((string.to_string(), true)), + Expression::Variable(var, _) => Some((var.to_string(), false)), + _ => None, + }; + } + } + } + None +} diff --git a/libs/sqf/src/analyze/required_version.rs b/libs/sqf/src/analyze/required_version.rs index f8c202fb..4bec931f 100644 --- a/libs/sqf/src/analyze/required_version.rs +++ b/libs/sqf/src/analyze/required_version.rs @@ -29,7 +29,7 @@ pub fn required_version( let (command, usage, usage_span) = statements.required_version(database); if wiki_version < usage { errors.push(Arc::new( - super::codes::sae1_require_version::InsufficientRequiredVersion::new( + super::codes::sae1_require_version_command::InsufficientRequiredVersionCommand::new( command, usage_span, usage, diff --git a/libs/sqf/src/parser/database/mod.rs b/libs/sqf/src/parser/database/mod.rs index 6e3fcf51..76c0b4e6 100644 --- a/libs/sqf/src/parser/database/mod.rs +++ b/libs/sqf/src/parser/database/mod.rs @@ -7,7 +7,7 @@ use arma3_wiki::{ Wiki, }; use hemtt_workspace::WorkspacePath; -use tracing::{trace, warn}; +use tracing::{error, trace, warn}; use crate::Error; @@ -57,22 +57,22 @@ pub struct Database { impl Database { #[must_use] /// An empty database with no entries. - pub fn empty() -> Self { + pub fn empty(force_pull: bool) -> Self { Self { nular_commands: HashSet::new(), unary_commands: HashSet::new(), binary_commands: HashSet::new(), - wiki: load_wiki(), + wiki: load_wiki(force_pull), } } #[must_use] - pub fn a3() -> Self { + pub fn a3(force_pull: bool) -> Self { let mut nular_commands = HashSet::new(); let mut unary_commands = HashSet::new(); let mut binary_commands = HashSet::new(); - let wiki = load_wiki(); + let wiki = load_wiki(force_pull); for command in wiki.commands().values() { for syntax in command.syntax() { @@ -106,8 +106,8 @@ impl Database { } /// Creates a new database with the default commands and custom commands from the workspace. - pub fn a3_with_workspace(workspace: &WorkspacePath) -> Result { - let mut database = Self::a3(); + pub fn a3_with_workspace(workspace: &WorkspacePath, force_pull: bool) -> Result { + let mut database = Self::a3(force_pull); let custom_root = workspace.join("/.hemtt/commands"); if let Ok(custom_root) = custom_root { if custom_root.exists().unwrap_or(false) { @@ -260,10 +260,14 @@ fn is_in(list: &[&str], item: &str) -> bool { list.iter().any(|i| i.eq_ignore_ascii_case(item)) } -fn load_wiki() -> Wiki { - Wiki::load_git().unwrap_or_else(|e| { +fn load_wiki(force_pull: bool) -> Wiki { + Wiki::load_git(force_pull).unwrap_or_else(|e| { trace!(?e, "failed to load arma 3 wiki from git: {}", e); - warn!("Failed to load Arma 3 wiki from git, falling back to bundled version"); + if force_pull { + error!("Failed to update Arma 3 wiki from remote"); + } else { + warn!("Failed to load Arma 3 wiki from git, falling back to bundled version"); + } Wiki::load_dist() }) } diff --git a/libs/sqf/tests/analyze.rs b/libs/sqf/tests/analyze.rs index 333ab9d2..e5d19912 100644 --- a/libs/sqf/tests/analyze.rs +++ b/libs/sqf/tests/analyze.rs @@ -31,7 +31,7 @@ fn test_analyze(dir: &str) { .unwrap(); let source = workspace.join("source.sqf").unwrap(); let processed = Processor::run(&source).unwrap(); - let database = Database::a3(); + let database = Database::a3(false); let workspace_files = WorkspaceFiles::new(); match hemtt_sqf::parser::run(&database, &processed) { Ok(sqf) => { diff --git a/libs/sqf/tests/errors.rs b/libs/sqf/tests/errors.rs index aa113e5d..92544b63 100644 --- a/libs/sqf/tests/errors.rs +++ b/libs/sqf/tests/errors.rs @@ -31,7 +31,7 @@ fn errors(dir: &str) { .unwrap(); let source = workspace.join("source.sqf").unwrap(); let processed = Processor::run(&source).unwrap(); - let parsed = hemtt_sqf::parser::run(&Database::a3(), &processed).unwrap_err(); + let parsed = hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap_err(); let codes = parsed.codes(); let mut expected = Vec::new(); std::fs::File::open(folder.join("stderr.ansi")) diff --git a/libs/sqf/tests/preprocessor.rs b/libs/sqf/tests/preprocessor.rs index dfb149d7..191dca60 100644 --- a/libs/sqf/tests/preprocessor.rs +++ b/libs/sqf/tests/preprocessor.rs @@ -35,7 +35,7 @@ fn preprocess(file: &str) { processed.as_str(), ) .unwrap(); - let parsed = hemtt_sqf::parser::run(&Database::a3(), &processed).unwrap(); + let parsed = hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap(); assert_ne!(parsed.content().len(), 0); let mut buffer = Vec::new(); parsed.compile_to_writer(&processed, &mut buffer).unwrap(); diff --git a/libs/sqf/tests/simple.rs b/libs/sqf/tests/simple.rs index 6ea3b173..c3883e63 100644 --- a/libs/sqf/tests/simple.rs +++ b/libs/sqf/tests/simple.rs @@ -31,7 +31,7 @@ fn simple(file: &str) { let source = workspace.join(format!("{file}.sqf")).unwrap(); let processed = Processor::run(&source).unwrap(); std::fs::write(format!("tests/simple/{file}.sqfp"), processed.as_str()).unwrap(); - let parsed = match hemtt_sqf::parser::run(&Database::a3(), &processed) { + let parsed = match hemtt_sqf::parser::run(&Database::a3(false), &processed) { Ok(sqf) => sqf, Err(hemtt_sqf::parser::ParserError::ParsingError(e)) => { for error in e {