use crate::model::{SemanticIndex, SymbolDefinition, SymbolKind};
use std::collections::{HashMap, HashSet};
pub trait VanillaSymbolSource {
fn visit_vanilla_definitions(&self, visit: &mut dyn FnMut(&SymbolDefinition));
}
impl VanillaSymbolSource for SemanticIndex {
fn visit_vanilla_definitions(&self, visit: &mut dyn FnMut(&SymbolDefinition)) {
for definition in &self.definitions {
visit(definition);
}
}
}
#[derive(Clone, Debug, Default)]
pub struct VanillaSymbolIndex {
by_kind: HashMap<SymbolKind, HashSet<String>>,
}
impl VanillaSymbolIndex {
pub fn build(source: &impl VanillaSymbolSource) -> Self {
let mut index = Self::default();
source.visit_vanilla_definitions(&mut |definition| {
index.insert_definition(definition);
});
index
}
pub fn from_semantic_index(index: &SemanticIndex) -> Self {
Self::build(index)
}
pub fn contains(&self, kind: SymbolKind, name: &str) -> bool {
self.by_kind
.get(&kind)
.is_some_and(|symbols| symbols.contains(name))
}
pub fn kinds_resolving(&self, name: &str) -> Vec<SymbolKind> {
let mut kinds = self
.by_kind
.iter()
.filter_map(|(kind, symbols)| symbols.contains(name).then_some(*kind))
.collect::<Vec<_>>();
kinds.sort_by_key(|kind| symbol_kind_order(*kind));
kinds
}
fn insert_definition(&mut self, definition: &SymbolDefinition) {
self.insert(definition.kind, definition.name.as_str());
if should_index_local_name(definition) {
self.insert(definition.kind, definition.local_name.as_str());
}
}
fn insert(&mut self, kind: SymbolKind, name: &str) {
if name.is_empty() {
return;
}
self.by_kind
.entry(kind)
.or_default()
.insert(name.to_string());
}
}
fn should_index_local_name(definition: &SymbolDefinition) -> bool {
definition.kind != SymbolKind::Event
&& !definition.local_name.is_empty()
&& definition.local_name != definition.name
}
fn symbol_kind_order(kind: SymbolKind) -> u8 {
match kind {
SymbolKind::ScriptedEffect => 0,
SymbolKind::ScriptedTrigger => 1,
SymbolKind::Event => 2,
SymbolKind::Decision => 3,
SymbolKind::DiplomaticAction => 4,
SymbolKind::TriggeredModifier => 5,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{MaybeScope, ScopeSet, test_support};
struct TestSource {
base: Option<SemanticIndex>,
}
impl VanillaSymbolSource for TestSource {
fn visit_vanilla_definitions(&self, visit: &mut dyn FnMut(&SymbolDefinition)) {
let Some(base) = self.base.as_ref() else {
return;
};
for definition in &base.definitions {
visit(definition);
}
}
}
fn definition(kind: SymbolKind, name: &str, local_name: &str) -> SymbolDefinition {
test_support::install_defaults();
SymbolDefinition {
kind,
name: name.to_string(),
module: "vanilla".to_string(),
local_name: local_name.to_string(),
mod_id: "__game__eu4".to_string(),
path: crate::model::GamePathBuf::parse("common/vanilla.txt").expect("valid game path"),
line: 1,
column: 1,
scope_id: 0,
declared_this_type: MaybeScope::Unknown,
inferred_this_type: MaybeScope::Unknown,
inferred_this_mask: ScopeSet::EMPTY,
inferred_from_mask: ScopeSet::EMPTY,
inferred_root_mask: ScopeSet::EMPTY,
required_params: Vec::new(),
optional_params: Vec::new(),
param_contract: None,
scope_param_names: Vec::new(),
}
}
fn semantic_index(definitions: Vec<SymbolDefinition>) -> SemanticIndex {
SemanticIndex {
definitions,
..Default::default()
}
}
#[test]
fn vanilla_symbol_index_groups_by_kind() {
let source = TestSource {
base: Some(semantic_index(vec![
definition(SymbolKind::ScriptedEffect, "eu4::effects::shared", "shared"),
definition(
SymbolKind::ScriptedTrigger,
"eu4::triggers::shared",
"shared",
),
])),
};
let index = VanillaSymbolIndex::build(&source);
assert!(index.contains(SymbolKind::ScriptedEffect, "shared"));
assert!(index.contains(SymbolKind::ScriptedTrigger, "shared"));
assert!(!index.contains(SymbolKind::Event, "shared"));
}
#[test]
fn vanilla_symbol_index_contains_known_vanilla_symbol() {
let source = TestSource {
base: Some(semantic_index(vec![definition(
SymbolKind::ScriptedEffect,
"eu4::common.scripted_effects::vanilla_effect",
"vanilla_effect",
)])),
};
let index = VanillaSymbolIndex::build(&source);
assert!(index.contains(SymbolKind::ScriptedEffect, "vanilla_effect"));
assert!(index.contains(
SymbolKind::ScriptedEffect,
"eu4::common.scripted_effects::vanilla_effect"
));
}
#[test]
fn vanilla_symbol_index_returns_empty_when_source_has_no_base() {
let source = TestSource { base: None };
let index = VanillaSymbolIndex::build(&source);
assert!(!index.contains(SymbolKind::ScriptedEffect, "vanilla_effect"));
assert!(index.kinds_resolving("vanilla_effect").is_empty());
}
#[test]
fn vanilla_symbol_index_kinds_resolving_finds_cross_kind_collisions() {
let source = TestSource {
base: Some(semantic_index(vec![
definition(SymbolKind::ScriptedEffect, "eu4::effects::shared", "shared"),
definition(SymbolKind::Decision, "eu4::decisions::shared", "shared"),
definition(
SymbolKind::TriggeredModifier,
"eu4::modifiers::other",
"other",
),
])),
};
let index = VanillaSymbolIndex::build(&source);
assert_eq!(
index.kinds_resolving("shared"),
vec![SymbolKind::ScriptedEffect, SymbolKind::Decision]
);
}
}