diff --git a/libs/config/src/analyze/codes/ce8_duplicate_external.rs b/libs/config/src/analyze/codes/ce8_duplicate_external.rs new file mode 100644 index 00000000..c162a6f2 --- /dev/null +++ b/libs/config/src/analyze/codes/ce8_duplicate_external.rs @@ -0,0 +1,74 @@ +use hemtt_workspace::reporting::{Code, Diagnostic, Label, Processed}; + +use crate::Class; + +pub struct DuplicateExternal { + classes: Vec, + diagnostic: Option, +} + +impl Code for DuplicateExternal { + fn ident(&self) -> &'static str { + "CE8" + } + + fn message(&self) -> String { + "external class defined multiple times".to_string() + } + + fn label_message(&self) -> String { + "defined multiple times".to_string() + } + + fn help(&self) -> Option { + self.classes + .first() + .expect("at least one class") + .name() + .map(|parent| { + format!( + "remove all but the first definition of `class {};`", + parent.as_str(), + ) + }) + } + + fn diagnostic(&self) -> Option { + self.diagnostic.clone() + } +} + +impl DuplicateExternal { + pub fn new(classes: Vec, processed: &Processed) -> Self { + Self { + classes, + diagnostic: None, + } + .generate_processed(processed) + } + + fn generate_processed(mut self, processed: &Processed) -> Self { + let Some(name) = self.classes[0].name() else { + panic!("DuplicateExternal::generate_processed called on class without name"); + }; + self.diagnostic = Diagnostic::new_for_processed(&self, name.span.clone(), processed); + if let Some(diag) = &mut self.diagnostic { + for class in self.classes.iter().skip(1) { + let map = processed + .mapping(class.name().expect("class should have name").span.start) + .expect("mapping should exist"); + let file = processed.source(map.source()).expect("source should exist"); + diag.labels.push( + Label::secondary( + file.0.clone(), + map.original_start() + ..map.original_start() + + class.name().expect("class should have name").span.len(), + ) + .with_message("also defined here"), + ); + } + } + self + } +} diff --git a/libs/config/src/analyze/codes/mod.rs b/libs/config/src/analyze/codes/mod.rs index daa60222..1d0624b5 100644 --- a/libs/config/src/analyze/codes/mod.rs +++ b/libs/config/src/analyze/codes/mod.rs @@ -8,6 +8,7 @@ pub mod ce4_missing_semicolon; pub mod ce5_unexpected_array; pub mod ce6_expected_array; pub mod ce7_missing_parent; +pub mod ce8_duplicate_external; pub mod cw1_parent_case; pub mod cw2_magwell_missing_magazine; diff --git a/libs/config/src/analyze/config.rs b/libs/config/src/analyze/config.rs deleted file mode 100644 index d839c393..00000000 --- a/libs/config/src/analyze/config.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -use hemtt_common::project::ProjectConfig; -use hemtt_workspace::reporting::{Code, Processed}; - -use crate::{Class, Config, Ident, Item, Property, Str, Value}; - -use super::{ - codes::{ - ce3_duplicate_property::DuplicateProperty, ce7_missing_parent::MissingParent, - cw1_parent_case::ParentCase, cw2_magwell_missing_magazine::MagwellMissingMagazine, - }, - Analyze, -}; - -impl Analyze for Config { - fn warnings( - &self, - project: Option<&ProjectConfig>, - processed: &Processed, - ) -> Vec> { - let mut warnings = self - .0 - .iter() - .flat_map(|p| p.warnings(project, processed)) - .collect::>(); - let mut defined = HashMap::new(); - warnings.extend(external_parent_case_warn(&self.0, &mut defined, processed)); - if let Some(project) = project { - warnings.extend(magwell_missing_magazine(project, self, processed)); - } - warnings - } - - fn errors(&self, project: Option<&ProjectConfig>, processed: &Processed) -> Vec> { - let mut errors = self - .0 - .iter() - .flat_map(|p| p.errors(project, processed)) - .collect::>(); - let mut defined = HashSet::new(); - errors.extend(external_missing_error(&self.0, &mut defined, processed)); - errors.extend(duplicate_properties(&self.0, processed)); - errors - } -} - -fn external_missing_error( - properties: &[Property], - defined: &mut HashSet, - processed: &Processed, -) -> Vec> { - let mut errors: Vec> = Vec::new(); - for property in properties { - if let Property::Class(c) = property { - match c { - Class::Root { properties } => { - errors.extend(external_missing_error(properties, defined, processed)); - } - Class::External { name } => { - let name = name.value.to_lowercase(); - if !defined.contains(&name) { - defined.insert(name); - } - } - Class::Local { - name, - parent, - properties, - } => { - let name = name.value.to_lowercase(); - if let Some(parent) = parent { - let parent = parent.value.to_lowercase(); - if parent != name && !defined.contains(&parent) { - errors.push(Arc::new(MissingParent::new(c.clone(), processed))); - } - } - defined.insert(name); - errors.extend(external_missing_error(properties, defined, processed)); - } - } - } - } - errors -} - -fn external_parent_case_warn( - properties: &[Property], - defined: &mut HashMap, - processed: &Processed, -) -> Vec> { - let mut warnings: Vec> = Vec::new(); - for property in properties { - if let Property::Class(c) = property { - match c { - Class::Root { .. } => { - panic!("Root class should not be in the config"); - } - Class::External { name } => { - let name = name.value.to_lowercase(); - defined.entry(name).or_insert_with(|| c.clone()); - } - Class::Local { - name, - parent, - properties, - } => { - let name_lower = name.value.to_lowercase(); - if let Some(parent) = parent { - let parent_lower = parent.value.to_lowercase(); - if parent_lower != name_lower { - if let Some(parent_class) = defined.get(&parent_lower) { - if parent_class.name().map(|p| &p.value) != Some(&parent.value) { - warnings.push(Arc::new(ParentCase::new( - c.clone(), - parent_class.clone(), - processed, - ))); - } - } - } else if parent.value != name.value { - warnings.push(Arc::new(ParentCase::new( - c.clone(), - c.clone(), - processed, - ))); - } - } - defined.insert(name_lower, c.clone()); - warnings.extend(external_parent_case_warn(properties, defined, processed)); - } - } - } - } - warnings -} - -fn duplicate_properties(properties: &[Property], processed: &Processed) -> Vec> { - let mut seen: HashMap> = HashMap::new(); - duplicate_properties_inner("", properties, &mut seen); - let mut errors: Vec> = Vec::new(); - for (_, idents) in seen { - if idents.len() > 1 && !idents.iter().all(|(class, _)| *class) { - errors.push(Arc::new(DuplicateProperty::new( - idents.into_iter().map(|(_, i)| i).collect(), - processed, - ))); - } - } - errors -} - -fn duplicate_properties_inner( - scope: &str, - properties: &[Property], - seen: &mut HashMap>, -) { - for property in properties { - match property { - Property::Class(Class::Local { - name, properties, .. - }) => { - duplicate_properties_inner( - &format!("{}.{}", scope, name.value.to_lowercase()), - properties, - seen, - ); - let entry = seen - .entry(format!("{}.{}", scope, name.value.to_lowercase())) - .or_default(); - entry.push((true, name.clone())); - } - Property::Entry { name, .. } | Property::MissingSemicolon(name, _) => { - let entry = seen - .entry(format!("{}.{}", scope, name.value.to_lowercase())) - .or_default(); - entry.push((false, name.clone())); - } - _ => (), - } - } -} - -fn magwell_missing_magazine( - project: &ProjectConfig, - config: &Config, - processed: &Processed, -) -> Vec> { - let mut warnings: Vec> = Vec::new(); - let mut classes = Vec::new(); - let Some(Property::Class(Class::Local { - properties: magwells, - .. - })) = config - .0 - .iter() - .find(|p| p.name().value.to_lowercase() == "cfgmagazinewells") - else { - return warnings; - }; - let Some(Property::Class(Class::Local { - properties: magazines, - .. - })) = config - .0 - .iter() - .find(|p| p.name().value.to_lowercase() == "cfgmagazines") - else { - return warnings; - }; - for property in magazines { - if let Property::Class(Class::Local { name, .. }) = property { - classes.push(name); - } - } - for magwell in magwells { - let Property::Class(Class::Local { - properties: addons, .. - }) = magwell - else { - continue; - }; - for addon in addons { - let Property::Entry { - name, - value: Value::Array(magazines), - .. - } = addon - else { - continue; - }; - for mag in &magazines.items { - let Item::Str(Str { value, span }) = mag else { - continue; - }; - if !value - .to_lowercase() - .starts_with(&project.prefix().to_lowercase()) - { - continue; - } - if !classes.iter().any(|c| c.value == *value) { - warnings.push(Arc::new(MagwellMissingMagazine::new( - name.clone(), - span.clone(), - processed, - ))); - } - } - } - } - warnings -} diff --git a/libs/config/src/analyze/config/duplicate_properties.rs b/libs/config/src/analyze/config/duplicate_properties.rs new file mode 100644 index 00000000..f045594b --- /dev/null +++ b/libs/config/src/analyze/config/duplicate_properties.rs @@ -0,0 +1,51 @@ +use std::{collections::HashMap, sync::Arc}; + +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::{analyze::codes::ce3_duplicate_property::DuplicateProperty, Class, Ident, Property}; + +pub fn duplicate_properties(properties: &[Property], processed: &Processed) -> Vec> { + let mut seen: HashMap> = HashMap::new(); + duplicate_properties_inner("", properties, &mut seen); + let mut errors: Vec> = Vec::new(); + for (_, idents) in seen { + if idents.len() > 1 && !idents.iter().all(|(class, _)| *class) { + errors.push(Arc::new(DuplicateProperty::new( + idents.into_iter().map(|(_, i)| i).collect(), + processed, + ))); + } + } + errors +} + +fn duplicate_properties_inner( + scope: &str, + properties: &[Property], + seen: &mut HashMap>, +) { + for property in properties { + match property { + Property::Class(Class::Local { + name, properties, .. + }) => { + duplicate_properties_inner( + &format!("{}.{}", scope, name.value.to_lowercase()), + properties, + seen, + ); + let entry = seen + .entry(format!("{}.{}", scope, name.value.to_lowercase())) + .or_default(); + entry.push((true, name.clone())); + } + Property::Entry { name, .. } | Property::MissingSemicolon(name, _) => { + let entry = seen + .entry(format!("{}.{}", scope, name.value.to_lowercase())) + .or_default(); + entry.push((false, name.clone())); + } + _ => (), + } + } +} diff --git a/libs/config/src/analyze/config/external_duplicate.rs b/libs/config/src/analyze/config/external_duplicate.rs new file mode 100644 index 00000000..4964f713 --- /dev/null +++ b/libs/config/src/analyze/config/external_duplicate.rs @@ -0,0 +1,33 @@ +use std::{collections::HashMap, sync::Arc}; + +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::{analyze::codes::ce8_duplicate_external::DuplicateExternal, Class, Property}; + +pub fn error(properties: &[Property], processed: &Processed) -> Vec> { + let mut defined: HashMap> = HashMap::new(); + let mut errors = Vec::new(); + for property in properties { + if let Property::Class(c) = property { + match c { + Class::Root { properties } | Class::Local { properties, .. } => { + errors.extend(error(properties, processed)); + } + Class::External { name } => { + defined + .entry(name.value.to_lowercase()) + .or_default() + .push(c.clone()); + } + } + } + } + errors.extend(defined.into_iter().filter_map(|(_, classes)| { + if classes.len() > 1 { + Some(Arc::new(DuplicateExternal::new(classes, processed)) as Arc) + } else { + None + } + })); + errors +} diff --git a/libs/config/src/analyze/config/external_missing.rs b/libs/config/src/analyze/config/external_missing.rs new file mode 100644 index 00000000..1766321a --- /dev/null +++ b/libs/config/src/analyze/config/external_missing.rs @@ -0,0 +1,48 @@ +use std::{collections::HashSet, sync::Arc}; + +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::{analyze::codes::ce7_missing_parent::MissingParent, Class, Property}; + +pub fn error(properties: &[Property], processed: &Processed) -> Vec> { + error_inner(properties, &mut HashSet::new(), processed) +} + +fn error_inner( + properties: &[Property], + defined: &mut HashSet, + processed: &Processed, +) -> Vec> { + let mut errors: Vec> = Vec::new(); + for property in properties { + if let Property::Class(c) = property { + match c { + Class::Root { properties } => { + errors.extend(error_inner(properties, defined, processed)); + } + Class::External { name } => { + let name = name.value.to_lowercase(); + if !defined.contains(&name) { + defined.insert(name); + } + } + Class::Local { + name, + parent, + properties, + } => { + let name = name.value.to_lowercase(); + if let Some(parent) = parent { + let parent = parent.value.to_lowercase(); + if parent != name && !defined.contains(&parent) { + errors.push(Arc::new(MissingParent::new(c.clone(), processed))); + } + } + defined.insert(name); + errors.extend(error_inner(properties, defined, processed)); + } + } + } + } + errors +} diff --git a/libs/config/src/analyze/config/external_parent_case.rs b/libs/config/src/analyze/config/external_parent_case.rs new file mode 100644 index 00000000..bbc1efa2 --- /dev/null +++ b/libs/config/src/analyze/config/external_parent_case.rs @@ -0,0 +1,60 @@ +use std::{collections::HashMap, sync::Arc}; + +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::{analyze::codes::cw1_parent_case::ParentCase, Class, Property}; + +pub fn warn(properties: &[Property], processed: &Processed) -> Vec> { + warn_inner(properties, &mut HashMap::new(), processed) +} + +fn warn_inner( + properties: &[Property], + defined: &mut HashMap, + processed: &Processed, +) -> Vec> { + let mut warnings: Vec> = Vec::new(); + for property in properties { + if let Property::Class(c) = property { + match c { + Class::Root { .. } => { + panic!("Root class should not be in the config"); + } + Class::External { name } => { + let name = name.value.to_lowercase(); + defined.entry(name).or_insert_with(|| c.clone()); + } + Class::Local { + name, + parent, + properties, + } => { + let name_lower = name.value.to_lowercase(); + if let Some(parent) = parent { + let parent_lower = parent.value.to_lowercase(); + if parent_lower != name_lower { + if let Some(parent_class) = defined.get(&parent_lower) { + if parent_class.name().map(|p| &p.value) != Some(&parent.value) { + warnings.push(Arc::new(ParentCase::new( + c.clone(), + parent_class.clone(), + processed, + ))); + } + } + } else if parent.value != name.value { + warnings.push(Arc::new(ParentCase::new( + c.clone(), + c.clone(), + processed, + ))); + } + } + defined.insert(name_lower, c.clone()); + warnings.extend(warn_inner(properties, defined, processed)); + } + } + } + } + warnings +} diff --git a/libs/config/src/analyze/config/magwells.rs b/libs/config/src/analyze/config/magwells.rs new file mode 100644 index 00000000..6048a6fa --- /dev/null +++ b/libs/config/src/analyze/config/magwells.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use hemtt_common::project::ProjectConfig; +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::{ + analyze::codes::cw2_magwell_missing_magazine::MagwellMissingMagazine, Class, Config, Item, + Property, Str, Value, +}; + +pub fn missing_magazine( + project: &ProjectConfig, + config: &Config, + processed: &Processed, +) -> Vec> { + let mut warnings: Vec> = Vec::new(); + let mut classes = Vec::new(); + let Some(Property::Class(Class::Local { + properties: magwells, + .. + })) = config + .0 + .iter() + .find(|p| p.name().value.to_lowercase() == "cfgmagazinewells") + else { + return warnings; + }; + let Some(Property::Class(Class::Local { + properties: magazines, + .. + })) = config + .0 + .iter() + .find(|p| p.name().value.to_lowercase() == "cfgmagazines") + else { + return warnings; + }; + for property in magazines { + if let Property::Class(Class::Local { name, .. }) = property { + classes.push(name); + } + } + for magwell in magwells { + let Property::Class(Class::Local { + properties: addons, .. + }) = magwell + else { + continue; + }; + for addon in addons { + let Property::Entry { + name, + value: Value::Array(magazines), + .. + } = addon + else { + continue; + }; + for mag in &magazines.items { + let Item::Str(Str { value, span }) = mag else { + continue; + }; + if !value + .to_lowercase() + .starts_with(&project.prefix().to_lowercase()) + { + continue; + } + if !classes.iter().any(|c| c.value == *value) { + warnings.push(Arc::new(MagwellMissingMagazine::new( + name.clone(), + span.clone(), + processed, + ))); + } + } + } + } + warnings +} diff --git a/libs/config/src/analyze/config/mod.rs b/libs/config/src/analyze/config/mod.rs new file mode 100644 index 00000000..ba94bc41 --- /dev/null +++ b/libs/config/src/analyze/config/mod.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use hemtt_common::project::ProjectConfig; +use hemtt_workspace::reporting::{Code, Processed}; + +use crate::Config; + +use super::Analyze; + +mod duplicate_properties; +mod external_duplicate; +mod external_missing; +mod external_parent_case; +mod magwells; + +impl Analyze for Config { + fn warnings( + &self, + project: Option<&ProjectConfig>, + processed: &Processed, + ) -> Vec> { + let mut warnings = self + .0 + .iter() + .flat_map(|p| p.warnings(project, processed)) + .collect::>(); + warnings.extend(external_parent_case::warn(&self.0, processed)); + if let Some(project) = project { + warnings.extend(magwells::missing_magazine(project, self, processed)); + } + warnings + } + + fn errors(&self, project: Option<&ProjectConfig>, processed: &Processed) -> Vec> { + let mut errors = self + .0 + .iter() + .flat_map(|p| p.errors(project, processed)) + .collect::>(); + errors.extend(external_duplicate::error(&self.0, processed)); + errors.extend(external_missing::error(&self.0, processed)); + errors.extend(duplicate_properties::duplicate_properties( + &self.0, processed, + )); + errors + } +} diff --git a/libs/config/tests/errors.rs b/libs/config/tests/errors.rs index 198bfc47..d77fc470 100644 --- a/libs/config/tests/errors.rs +++ b/libs/config/tests/errors.rs @@ -74,3 +74,4 @@ bootstrap!(ce4_missing_semicolon); bootstrap!(ce5_unexpected_array); bootstrap!(ce6_expected_array); bootstrap!(ce7_missing_parent); +bootstrap!(ce8_duplicate_external); diff --git a/libs/config/tests/errors/ce8_duplicate_external/source.hpp b/libs/config/tests/errors/ce8_duplicate_external/source.hpp new file mode 100644 index 00000000..e7891a64 --- /dev/null +++ b/libs/config/tests/errors/ce8_duplicate_external/source.hpp @@ -0,0 +1,10 @@ +class CfgAmmo { + class BulletBase; + class BulletBase; + class test: BulletBase { + class BulletInfo; + }; + class test2: BulletBase { + class BulletInfo; + }; +}; diff --git a/libs/config/tests/errors/ce8_duplicate_external/stdout.ansi b/libs/config/tests/errors/ce8_duplicate_external/stdout.ansi new file mode 100644 index 00000000..a9e5218f --- /dev/null +++ b/libs/config/tests/errors/ce8_duplicate_external/stdout.ansi @@ -0,0 +1,10 @@ +error[CE8]: external class defined multiple times + ┌─ source.hpp:2:11 + │ +2 │ class BulletBase; + │ ^^^^^^^^^^ defined multiple times +3 │ class BulletBase; + │ ---------- also defined here + │ + = help: remove all but the first definition of `class BulletBase;` +