pub mod definition_module;
pub mod documents;
pub(crate) mod emit;
mod extractors;
pub mod localisation;
pub mod parse_cache;
pub mod parser;
pub use self::emit::emit_native_statements;
pub(crate) use self::extractors::is_clausewitz_date_key;
use self::localisation::collect_localisation_definitions_from_root;
use self::parser::{AstFile, AstStatement, AstValue, ParseResult, SpanRange, read_failure};
use super::analysis::param_contracts::{
apply_registered_param_contracts, explicit_contract_param_names, registered_param_contract,
};
use super::base::builtin::{
is_builtin_effect, is_builtin_iterator, is_builtin_scope_changer, is_builtin_special_block,
is_builtin_trigger, is_contextual_keyword, is_game_only_candidate, is_reserved_keyword,
};
use super::content::{
ContentFamilyDescriptor, ContentFamilyScopePolicy, ScriptFileKind, eu4,
module_name_for_descriptor,
};
use super::cwt::{
hand_container_scope_fallback, is_country_file_reference, is_country_tag_selector,
is_country_tag_text, is_dynamic_scope_reference_key, is_province_id_selector,
is_province_id_text, looks_like_map_group_key, schema_file_kind_container_scope_kind,
schema_path_container_scope_kind,
};
pub use super::cwt::{iterator_scope_type, scope_changer_target_type, special_block_scope_kind};
use crate::game::schema::query::CwtQuery;
use crate::model::{
AliasUsage, DocumentFamily, DocumentRecord, GamePath, GamePathBuf, KeyUsage,
LocalisationDefinition, MaybeScope, ParamBinding, ParseIssue, ResourceReference,
ScalarAssignment, ScopeKind, ScopeNode, ScopeSet, ScopeType, SemanticIndex, SourceSpan,
SymbolDefinition, SymbolKind, SymbolReference, UiDefinition, base_scope, has_fatal_parse_issue,
};
use rayon::prelude::*;
use regex::Regex;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[derive(Clone, Debug)]
pub struct ParsedScriptFile {
pub mod_id: String,
pub path: Option<PathBuf>,
pub relative_path: GamePathBuf,
pub content_family: Option<&'static ContentFamilyDescriptor>,
pub file_kind: ScriptFileKind,
pub module_name: String,
pub ast: AstFile,
pub source: String,
pub parse_issues: Vec<ParseIssue>,
pub parse_cache_hit: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct ParsedScriptInputIdentity {
pub size_bytes: u64,
pub content_digest: String,
}
pub(super) struct ParsedScriptWithInputIdentity {
pub file: ParsedScriptFile,
pub input_identity: Option<ParsedScriptInputIdentity>,
}
use parse_cache::{parse_clausewitz_bytes_cached, parse_clausewitz_for_path};
pub fn classify_script_file(relative: &GamePath) -> ScriptFileKind {
eu4()
.classify_content_family(relative)
.map_or(ScriptFileKind::new("other"), |descriptor| {
descriptor.script_file_kind.clone()
})
}
#[derive(Default)]
struct MapGroupLookup {
province_sets: HashSet<String>,
}
impl MapGroupLookup {
fn contains(&self, key: &str) -> bool {
self.province_sets.contains(key)
}
}
fn collect_map_groups(files: &[ParsedScriptFile]) -> MapGroupLookup {
let mut lookup = MapGroupLookup::default();
for file in files {
if !is_map_group_file(&file.relative_path) {
continue;
}
for stmt in &file.ast.statements {
if let AstStatement::Assignment {
key,
value: AstValue::Block { .. },
..
} = stmt && !is_keyword(key)
{
lookup.province_sets.insert(key.clone());
}
}
}
lookup
}
fn is_map_group_file(relative_path: &GamePath) -> bool {
matches!(
relative_path.as_str(),
"map/area.txt"
| "map/region.txt"
| "map/superregion.txt"
| "map/continent.txt"
| "map/provincegroup.txt"
) || relative_path.is_inside(&["common", "trade_companies"], str::eq)
}
fn fallback_module_name_from_relative(relative: &GamePath) -> String {
relative.parent().map_or_else(
|| "other".to_string(),
|parent| parent.iter().collect::<Vec<_>>().join("."),
)
}
fn qualify_symbol_name(module: &str, local: &str) -> String {
format!("eu4::{module}::{local}")
}
pub fn parse_script_file(mod_id: &str, root: &Path, relative: &GamePath) -> ParsedScriptFile {
parse_script_file_with_cache_and_input_identity(mod_id, root, relative, true).file
}
pub(super) fn parse_script_file_with_input_identity(
mod_id: &str,
root: &Path,
relative: &GamePath,
) -> ParsedScriptWithInputIdentity {
parse_script_file_with_cache_and_input_identity(mod_id, root, relative, true)
}
pub(super) fn parse_script_file_without_cache_with_input_identity(
mod_id: &str,
root: &Path,
relative: &GamePath,
) -> ParsedScriptWithInputIdentity {
parse_script_file_with_cache_and_input_identity(mod_id, root, relative, false)
}
pub fn parse_script_bytes_cached(
mod_id: &str,
root: &Path,
relative: &GamePath,
bytes: &[u8],
) -> ParsedScriptFile {
let (parsed, parse_cache_hit) = parse_clausewitz_bytes_cached(relative, bytes);
parsed_script_file_from_result(
mod_id,
Some(relative.to_path(root)),
parsed,
parse_cache_hit,
String::new(),
)
}
fn parse_script_file_with_cache_and_input_identity(
mod_id: &str,
root: &Path,
relative: &GamePath,
use_cache: bool,
) -> ParsedScriptWithInputIdentity {
let file = relative.to_path(root);
let (parsed, parse_cache_hit, source, input_identity) = match std::fs::read(&file) {
Ok(bytes) => {
let content_digest = blake3::hash(&bytes).to_hex().to_string();
let source = crate::game::eu4::text::decode_paradox_bytes(&bytes).into_owned();
let (parsed, parse_cache_hit) = if use_cache {
parse_clausewitz_bytes_cached(relative, &bytes)
} else {
(parse_clausewitz_for_path(relative, &source), false)
};
(
parsed,
parse_cache_hit,
source,
Some(ParsedScriptInputIdentity {
size_bytes: bytes.len() as u64,
content_digest,
}),
)
}
Err(err) => (
read_failure(&err).into_parse_result(relative.to_owned()),
false,
String::new(),
None,
),
};
ParsedScriptWithInputIdentity {
file: parsed_script_file_from_result(mod_id, Some(file), parsed, parse_cache_hit, source),
input_identity,
}
}
fn parsed_script_file_from_result(
mod_id: &str,
path: Option<PathBuf>,
parsed: ParseResult,
parse_cache_hit: bool,
source: String,
) -> ParsedScriptFile {
let relative = parsed.ast.path.clone();
let content_family = eu4().classify_content_family(&relative);
let file_kind = content_family.map_or(ScriptFileKind::new("other"), |descriptor| {
descriptor.script_file_kind.clone()
});
let module_name = content_family.map_or_else(
|| fallback_module_name_from_relative(&relative),
|descriptor| module_name_for_descriptor(&relative, descriptor).replace('-', "_"),
);
let parse_issues = parsed
.diagnostics
.into_iter()
.map(|item| ParseIssue {
mod_id: mod_id.to_string(),
path: relative.clone(),
line: item.span.start.line,
column: item.span.start.column,
message: item.message,
repair: item.repair,
isolation: item.isolation,
})
.collect();
ParsedScriptFile {
mod_id: mod_id.to_string(),
path,
relative_path: relative,
content_family,
file_kind,
module_name,
ast: parsed.ast,
source,
parse_issues,
parse_cache_hit,
}
}
pub fn collect_localisation_definitions(
mod_id: &str,
root: &Path,
) -> std::io::Result<Vec<LocalisationDefinition>> {
collect_localisation_definitions_from_root(mod_id, root)
}
pub fn build_semantic_index(files: &[ParsedScriptFile]) -> SemanticIndex {
let map_groups = collect_map_groups(files);
let cwt_rule_engine = Some(super::cwt::rule_engine());
let file_indexes =
files
.par_iter()
.map(|file| {
let mut index = SemanticIndex::default();
index.documents.push(DocumentRecord {
mod_id: file.mod_id.clone(),
path: file.relative_path.clone(),
family: DocumentFamily::Clausewitz,
parse_ok: !has_fatal_parse_issue(&file.parse_issues),
});
index.parse_issues.extend(file.parse_issues.clone());
build_file_index(file, &map_groups, cwt_rule_engine, &mut index);
let cultures =
super::cultures::collect_culture_references(&file.ast, &file.relative_path);
index
.resource_references
.extend(
cultures
.references
.into_iter()
.map(|reference| ResourceReference {
key: "culture_reference".to_owned(),
value: reference.id,
mod_id: file.mod_id.clone(),
path: reference.location.relative_path,
line: reference.location.span.start.line,
column: reference.location.span.start.column,
}),
);
index
.resource_references
.extend(cultures.group_references.into_iter().map(|reference| {
ResourceReference {
key: "culture_group_reference".to_owned(),
value: reference.group,
mod_id: file.mod_id.clone(),
path: reference.location.relative_path,
line: reference.location.span.start.line,
column: reference.location.span.start.column,
}
}));
index
})
.collect::<Vec<_>>();
let mut index = SemanticIndex::default();
for file_index in file_indexes {
index.append(file_index);
}
infer_definition_scope_from_references(&mut index);
infer_definition_dynamic_alias_masks_from_references(&mut index);
apply_registered_param_contracts(&mut index);
index
}
pub fn script_container_scope_kind(
file_kind: ScriptFileKind,
file_path: &GamePath,
ast_path: &[&str],
) -> Option<ScopeKind> {
let key = *ast_path.last()?;
schema_path_container_scope_kind(
super::cwt::rule_engine(),
file_kind.clone(),
file_path,
ast_path,
)
.or_else(|| hand_container_scope_fallback(file_kind, key))
}
fn build_file_index(
file: &ParsedScriptFile,
map_groups: &MapGroupLookup,
cwt_rule_engine: Option<&CwtQuery>,
index: &mut SemanticIndex,
) {
let mut aliases = BTreeMap::new();
let scope_policy = file
.content_family
.map_or(ContentFamilyScopePolicy::default(), |descriptor| {
descriptor.scope_policy
});
let root_this_type = scope_policy.root_scope;
aliases.insert("THIS".to_string(), root_this_type);
aliases.insert("ROOT".to_string(), root_this_type);
if let Some(from_alias) = scope_policy.from_alias {
aliases.insert("FROM".to_string(), from_alias.into());
}
let root_scope = push_scope(ScopeArgs {
index,
kind: ScopeKind::File,
parent: None,
this_type: aliases.get("THIS").copied().unwrap_or(MaybeScope::Unknown),
aliases,
mod_id: &file.mod_id,
path: &file.relative_path,
line: line_from_stmt(file.ast.statements.first()),
key: "",
});
let mut ctx = BuildContext {
mod_id: &file.mod_id,
path: &file.relative_path,
content_family: file.content_family,
file_kind: file.file_kind.clone(),
cwt_rule_engine,
module_name: &file.module_name,
source: &file.source,
map_groups,
technology_monarch_power: None,
technology_definition_ordinal: 0,
random_map_tile_emitted: false,
random_name_table_emitted: false,
};
walk_statements(
&file.ast.statements,
index,
root_scope,
&mut ctx,
None,
None,
);
}
fn event_scope_type(key: &str) -> Option<ScopeType> {
match key {
"country_event" => Some(base_scope::country()),
"province_event" => Some(base_scope::province()),
_ => None,
}
}
fn event_from_type(key: &str) -> Option<ScopeType> {
match key {
"country_event" | "province_event" => Some(base_scope::country()),
_ => None,
}
}
fn is_top_level_event_definition(
index: &SemanticIndex,
scope_id: usize,
key: &str,
value: &AstValue,
) -> bool {
scope_kind(index, scope_id) == ScopeKind::File
&& event_scope_type(key).is_some()
&& matches!(value, AstValue::Block { .. })
}
struct BuildContext<'a> {
mod_id: &'a str,
path: &'a GamePath,
content_family: Option<&'static ContentFamilyDescriptor>,
file_kind: ScriptFileKind,
cwt_rule_engine: Option<&'a CwtQuery>,
module_name: &'a str,
source: &'a str,
map_groups: &'a MapGroupLookup,
technology_monarch_power: Option<String>,
technology_definition_ordinal: usize,
random_map_tile_emitted: bool,
random_name_table_emitted: bool,
}
fn walk_statements(
statements: &[AstStatement],
index: &mut SemanticIndex,
scope_id: usize,
ctx: &mut BuildContext<'_>,
active_scripted_effect: Option<usize>,
namespace: Option<String>,
) {
let mut current_namespace = namespace;
for stmt in statements {
match stmt {
AstStatement::Assignment {
key,
key_span,
value,
..
} => {
if is_top_level_event_definition(index, scope_id, key, value) {
record_foundation_resource_semantics(
index, scope_id, ctx, key, key_span, value,
);
handle_event_block(index, scope_id, ctx, key, value, current_namespace.clone());
continue;
}
record_key_usage(index, scope_id, ctx, key, key_span);
record_scalar_assignment(index, scope_id, ctx, key, key_span, value);
record_ui_scalar_semantics(index, ctx, key, key_span, value);
record_foundation_resource_semantics(index, scope_id, ctx, key, key_span, value);
if key == "namespace"
&& let Some(value_text) = scalar_text(value)
{
current_namespace = Some(value_text);
}
if is_alias_key(key) {
record_alias_usage(index, scope_id, ctx, key, key_span);
}
record_alias_tokens_from_value(index, scope_id, ctx, value);
record_param_tokens(index, active_scripted_effect, value);
if is_event_call(key, value)
&& let Some(event_id) = extract_event_call_id(value)
{
index.references.push(SymbolReference {
kind: SymbolKind::Event,
name: event_id,
module: ctx.module_name.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id,
provided_params: Vec::new(),
param_bindings: Vec::new(),
});
}
if is_scripted_trigger_call_candidate(
ctx,
ctx.file_kind.clone(),
key,
scope_id,
index,
) {
index.references.push(SymbolReference {
kind: SymbolKind::ScriptedTrigger,
name: key.clone(),
module: ctx.module_name.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id,
provided_params: Vec::new(),
param_bindings: Vec::new(),
});
}
if let AstValue::Block { items, span } = value {
record_ui_block_semantics(index, ctx, key, key_span, items);
let definition_kind =
symbol_definition_kind(ctx.file_kind.clone(), key, scope_id, index);
let child_scope = create_child_scope(index, scope_id, ctx, key, span, items);
if let Some(def_kind) = definition_kind {
let optional_params = collect_optional_params_from_source(
ctx.source,
span.start.offset,
span.end.offset,
);
let optional_set: HashSet<&str> =
optional_params.iter().map(String::as_str).collect();
let mut required_params: Vec<String> = collect_required_params(items)
.into_iter()
.filter(|param| !optional_set.contains(param.as_str()))
.collect();
required_params.sort();
required_params.dedup();
index.definitions.push(SymbolDefinition {
kind: def_kind,
name: qualify_symbol_name(ctx.module_name, key),
module: ctx.module_name.to_string(),
local_name: key.clone(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id: child_scope,
declared_this_type: scope_this_type(index, child_scope),
inferred_this_type: MaybeScope::Unknown,
inferred_this_mask: ScopeSet::EMPTY,
inferred_from_mask: ScopeSet::EMPTY,
inferred_root_mask: ScopeSet::EMPTY,
required_params,
optional_params,
param_contract: registered_param_contract(key),
scope_param_names: collect_scope_param_names(items),
});
}
if definition_kind.is_none()
&& !is_mission_slot_definition(ctx.file_kind.clone(), items)
&& is_scripted_effect_call_candidate(
ctx,
ctx.file_kind.clone(),
key,
scope_id,
index,
) {
let mut provided = collect_provided_params(key, items);
provided.names.sort();
provided.names.dedup();
provided.bindings.sort_by(|lhs, rhs| {
(lhs.name.as_str(), lhs.value.as_str())
.cmp(&(rhs.name.as_str(), rhs.value.as_str()))
});
provided
.bindings
.dedup_by(|lhs, rhs| lhs.name == rhs.name && lhs.value == rhs.value);
index.references.push(SymbolReference {
kind: SymbolKind::ScriptedEffect,
name: key.clone(),
module: ctx.module_name.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id,
provided_params: provided.names,
param_bindings: provided.bindings,
});
}
let next_scripted_effect = if event_scope_type(key).is_some() {
None
} else if ctx.file_kind == ScriptFileKind::new("scripted_effects")
&& scope_kind(index, scope_id) == ScopeKind::File
{
find_scripted_effect_definition(index, ctx.mod_id, ctx.path, key)
} else {
active_scripted_effect
};
walk_statements(
items,
index,
child_scope,
ctx,
next_scripted_effect,
current_namespace.clone(),
);
}
}
AstStatement::Item { value, .. } => {
record_alias_tokens_from_value(index, scope_id, ctx, value);
record_param_tokens(index, active_scripted_effect, value);
if let AstValue::Block { items, span } = value {
let this_type = scope_this_type(index, scope_id);
let aliases = scope_aliases(index, scope_id);
let child_scope = push_scope(ScopeArgs {
index,
kind: ScopeKind::Block,
parent: Some(scope_id),
this_type,
aliases,
mod_id: ctx.mod_id,
path: ctx.path,
line: span.start.line,
key: "",
});
walk_statements(
items,
index,
child_scope,
ctx,
active_scripted_effect,
current_namespace.clone(),
);
}
}
AstStatement::Comment { .. } => {}
}
}
}
fn handle_event_block(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &BuildContext<'_>,
key: &str,
value: &AstValue,
namespace: Option<String>,
) {
let AstValue::Block { items, span } = value else {
return;
};
let Some(this_type) = event_scope_type(key) else {
return;
};
let from_type = event_from_type(key)
.map(MaybeScope::from)
.unwrap_or(MaybeScope::Unknown);
let mut aliases = scope_aliases(index, scope_id);
aliases.insert("THIS".to_string(), MaybeScope::Known(this_type));
aliases.insert("ROOT".to_string(), MaybeScope::Known(this_type));
aliases.insert("FROM".to_string(), from_type);
aliases.insert("PREV".to_string(), scope_this_type(index, scope_id));
let event_scope = push_scope(ScopeArgs {
index,
kind: ScopeKind::Event,
parent: Some(scope_id),
this_type: this_type.into(),
aliases,
mod_id: ctx.mod_id,
path: ctx.path,
line: span.start.line,
key,
});
if let Some(id) = extract_assignment_scalar(items, "id") {
let full_id = if id.contains('.') {
id
} else if let Some(ns) = namespace.as_ref() {
format!("{ns}.{id}")
} else {
id
};
index.definitions.push(SymbolDefinition {
kind: SymbolKind::Event,
name: full_id,
module: ctx.module_name.to_string(),
local_name: key.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: span.start.line,
column: span.start.column,
scope_id: event_scope,
declared_this_type: this_type.into(),
inferred_this_type: this_type.into(),
inferred_this_mask: scope_mask(this_type),
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(),
});
}
let mut child_ctx = BuildContext {
mod_id: ctx.mod_id,
path: ctx.path,
content_family: ctx.content_family,
file_kind: ctx.file_kind.clone(),
cwt_rule_engine: ctx.cwt_rule_engine,
module_name: ctx.module_name,
source: ctx.source,
map_groups: ctx.map_groups,
technology_monarch_power: ctx.technology_monarch_power.clone(),
technology_definition_ordinal: ctx.technology_definition_ordinal,
random_map_tile_emitted: ctx.random_map_tile_emitted,
random_name_table_emitted: ctx.random_name_table_emitted,
};
walk_statements(items, index, event_scope, &mut child_ctx, None, namespace);
}
fn record_key_usage(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &BuildContext<'_>,
key: &str,
key_span: &SpanRange,
) {
index.key_usages.push(KeyUsage {
key: key.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id,
this_type: scope_this_type(index, scope_id),
});
}
fn record_scalar_assignment(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &BuildContext<'_>,
key: &str,
key_span: &SpanRange,
value: &AstValue,
) {
let AstValue::Scalar { value, .. } = value else {
return;
};
index.scalar_assignments.push(ScalarAssignment {
key: key.to_string(),
value: value.as_text(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
scope_id,
});
}
fn record_ui_scalar_semantics(
index: &mut SemanticIndex,
ctx: &BuildContext<'_>,
key: &str,
key_span: &SpanRange,
value: &AstValue,
) {
if ctx.file_kind != ScriptFileKind::new("ui") {
return;
}
let Some(text) = scalar_text(value) else {
return;
};
if key == "name" && is_ui_identifier_candidate(&text) {
index.ui_definitions.push(UiDefinition {
name: text.clone(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
});
}
if is_ui_resource_key(key) {
index.resource_references.push(ResourceReference {
key: key.to_string(),
value: text,
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
});
}
}
fn record_foundation_resource_semantics(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &mut BuildContext<'_>,
key: &str,
key_span: &SpanRange,
value: &AstValue,
) {
let Some(descriptor) = ctx.content_family else {
return;
};
if let Some(extractor) = extractors::extractor_for(descriptor) {
extractor.extract(index, scope_id, ctx, key, key_span, value);
}
}
fn push_resource_reference(
index: &mut SemanticIndex,
ctx: &BuildContext<'_>,
key_span: &SpanRange,
key: &str,
value: &str,
) {
index.resource_references.push(ResourceReference {
key: key.to_string(),
value: value.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
});
}
fn is_top_level_named_block(
index: &SemanticIndex,
scope_id: usize,
key: &str,
value: &AstValue,
) -> bool {
scope_kind(index, scope_id) == ScopeKind::File
&& matches!(value, AstValue::Block { .. })
&& !is_keyword(key)
}
fn is_named_block_in_top_level_block(
index: &SemanticIndex,
scope_id: usize,
key: &str,
value: &AstValue,
) -> bool {
scope_kind(index, scope_id) == ScopeKind::Block
&& scope_parent_kind(index, scope_id) == Some(ScopeKind::File)
&& matches!(value, AstValue::Block { .. })
&& !is_keyword(key)
}
fn scope_parent_kind(index: &SemanticIndex, scope_id: usize) -> Option<ScopeKind> {
let parent = index.scopes.get(scope_id)?.parent?;
Some(scope_kind(index, parent))
}
fn monarch_power_prefix(value: &str) -> Option<&'static str> {
match value {
"ADM" => Some("adm"),
"DIP" => Some("dip"),
"MIL" => Some("mil"),
_ => None,
}
}
fn extract_block_scalar_items(value: &AstValue) -> Vec<String> {
let AstValue::Block { items, .. } = value else {
return Vec::new();
};
let mut values: Vec<String> = Vec::new();
for item in items {
match item {
AstStatement::Assignment { value, .. } | AstStatement::Item { value, .. } => {
if let Some(text) = scalar_text(value) {
values.push(text);
}
}
AstStatement::Comment { .. } => {}
}
}
values
}
fn extract_named_block_scalar_items(value: &AstValue, key_name: &str) -> Vec<String> {
let AstValue::Block { items, .. } = value else {
return Vec::new();
};
let mut values: Vec<String> = Vec::new();
for item in items {
let AstStatement::Assignment { key, value, .. } = item else {
continue;
};
if key != key_name {
continue;
}
if let Some(text) = scalar_text(value) {
values.push(text);
continue;
}
values.extend(extract_block_scalar_items(value));
}
values
}
fn extract_named_block_member_keys(value: &AstValue, key_name: &str) -> Vec<String> {
let AstValue::Block { items, .. } = value else {
return Vec::new();
};
let mut keys: Vec<String> = Vec::new();
for item in items {
let AstStatement::Assignment { key, value, .. } = item else {
continue;
};
if key != key_name {
continue;
}
let AstValue::Block { items, .. } = value else {
continue;
};
for nested in items {
let AstStatement::Assignment { key, .. } = nested else {
continue;
};
keys.push(key.clone());
}
}
keys
}
fn extract_yes_assignment_keys(value: &AstValue) -> Vec<String> {
let AstValue::Block { items, .. } = value else {
return Vec::new();
};
let mut keys: Vec<String> = Vec::new();
for item in items {
let AstStatement::Assignment { key, value, .. } = item else {
continue;
};
let Some(text) = scalar_text(value) else {
continue;
};
if text.eq_ignore_ascii_case("yes") {
keys.push(key.clone());
}
}
keys
}
fn extract_nested_assignment_scalar(
items: &[AstStatement],
block_name: &str,
field_name: &str,
) -> Option<String> {
for item in items {
let AstStatement::Assignment { key, value, .. } = item else {
continue;
};
if key != block_name {
continue;
}
let AstValue::Block { items, .. } = value else {
continue;
};
if let Some(text) = extract_assignment_scalar(items, field_name) {
return Some(text);
}
}
None
}
fn record_ui_block_semantics(
index: &mut SemanticIndex,
ctx: &BuildContext<'_>,
key: &str,
key_span: &SpanRange,
items: &[AstStatement],
) {
if ctx.file_kind != ScriptFileKind::new("ui") || !looks_like_ui_container_key(key) {
return;
}
let Some(name) = extract_assignment_scalar(items, "name") else {
return;
};
if !is_ui_identifier_candidate(&name) {
return;
}
index.ui_definitions.push(UiDefinition {
name,
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: key_span.start.line,
column: key_span.start.column,
});
}
fn record_alias_usage(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &BuildContext<'_>,
alias: &str,
span: &SpanRange,
) {
index.alias_usages.push(AliasUsage {
alias: alias.to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: span.start.line,
column: span.start.column,
scope_id,
});
}
fn record_alias_tokens_from_value(
index: &mut SemanticIndex,
scope_id: usize,
ctx: &BuildContext<'_>,
value: &AstValue,
) {
let AstValue::Scalar { value, span } = value else {
return;
};
let text = value.as_text();
for cap in alias_capture_regex().captures_iter(&text) {
let Some(alias) = cap.get(1) else {
continue;
};
index.alias_usages.push(AliasUsage {
alias: alias.as_str().to_string(),
mod_id: ctx.mod_id.to_string(),
path: ctx.path.to_owned(),
line: span.start.line,
column: span.start.column,
scope_id,
});
}
}
fn record_param_tokens(index: &mut SemanticIndex, def_idx: Option<usize>, value: &AstValue) {
let Some(def_idx) = def_idx else {
return;
};
match value {
AstValue::Scalar { value, .. } => {
let text = value.as_text();
for cap in param_capture_regex().captures_iter(&text) {
let Some(param) = cap.get(1) else {
continue;
};
let param = param.as_str().to_string();
if let Some(def) = index.definitions.get_mut(def_idx)
&& !def.required_params.contains(¶m)
&& !def.optional_params.contains(¶m)
{
def.required_params.push(param);
}
}
}
AstValue::Block { items, .. } => {
for item in items {
match item {
AstStatement::Assignment { value, .. } | AstStatement::Item { value, .. } => {
record_param_tokens(index, Some(def_idx), value)
}
AstStatement::Comment { .. } => {}
}
}
}
}
}
fn alias_capture_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"\b(ROOT|FROM|THIS|PREV)\b").expect("valid alias regex"))
}
fn param_capture_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)\$").expect("valid param regex"))
}
fn top_level_symbol_kind(
file_kind: ScriptFileKind,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> Option<SymbolKind> {
if scope_kind(index, scope_id) != ScopeKind::File {
return None;
}
match file_kind.as_str() {
"scripted_effects" if !is_keyword(key) => Some(SymbolKind::ScriptedEffect),
"scripted_triggers" if !is_keyword(key) => Some(SymbolKind::ScriptedTrigger),
"decisions" if !is_keyword(key) && !is_decision_container_key(key) => {
Some(SymbolKind::Decision)
}
"diplomatic_actions" if !is_keyword(key) => Some(SymbolKind::DiplomaticAction),
"new_diplomatic_actions" if !is_keyword(key) && !matches!(key, "static_actions") => {
Some(SymbolKind::DiplomaticAction)
}
"triggered_modifiers" if !is_keyword(key) => Some(SymbolKind::TriggeredModifier),
_ => None,
}
}
fn symbol_definition_kind(
file_kind: ScriptFileKind,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> Option<SymbolKind> {
if let Some(kind) = top_level_symbol_kind(file_kind.clone(), key, scope_id, index) {
return Some(kind);
}
if file_kind == ScriptFileKind::new("decisions")
&& is_decision_entry_scope(index, scope_id)
&& !is_keyword(key)
{
return Some(SymbolKind::Decision);
}
None
}
fn is_decision_entry_scope(index: &SemanticIndex, scope_id: usize) -> bool {
let Some(scope) = index.scopes.get(scope_id) else {
return false;
};
if scope.kind != ScopeKind::Block {
return false;
}
let Some(parent_scope_id) = scope.parent else {
return false;
};
scope_kind(index, parent_scope_id) == ScopeKind::File
}
fn is_scripted_effect_call_candidate(
ctx: &BuildContext<'_>,
file_kind: ScriptFileKind,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> bool {
if is_keyword(key) || is_alias_key(key) {
return false;
}
if is_template_param_placeholder_key(key) || key.contains('$') || key.contains('[') {
return false;
}
if key.parse::<u32>().is_ok() {
return false;
}
if is_dynamic_scope_reference_key(key) {
return false;
}
if scope_kind(index, scope_id) == ScopeKind::File {
return false;
}
if is_data_context(index, scope_id) {
return false;
}
if is_param_block_scope(index, scope_id) {
return false;
}
if is_province_id_selector(key) || is_country_tag_selector(key) {
return false;
}
if effect_context_scope_semantics(ctx, key, scope_id, index).is_some() {
return false;
}
if is_map_group_scope_key(ctx, key, scope_id, index) {
return false;
}
if is_builtin_effect(key)
|| is_builtin_trigger(key)
|| is_builtin_scope_changer(key)
|| is_builtin_iterator(key)
|| is_builtin_special_block(key)
|| is_game_only_candidate(key)
{
return false;
}
if !is_effect_like_scope(index, scope_id) {
return false;
}
if !allows_generic_scripted_effect_fallback(scope_kind(index, scope_id)) {
return false;
}
if file_kind == ScriptFileKind::new("missions") {
}
if file_kind == ScriptFileKind::new("decisions") && is_decision_entry_scope(index, scope_id) {
return false;
}
if file_kind == ScriptFileKind::new("missions") && is_mission_slot_scope(index, scope_id) {
return false;
}
if file_kind == ScriptFileKind::new("scripted_effects")
&& scope_kind(index, scope_id) == ScopeKind::File
{
return false;
}
true
}
fn is_scripted_trigger_call_candidate(
ctx: &BuildContext<'_>,
file_kind: ScriptFileKind,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> bool {
if is_keyword(key) || is_alias_key(key) {
return false;
}
if is_template_param_placeholder_key(key) || key.contains('$') || key.contains('[') {
return false;
}
if key.parse::<u32>().is_ok() {
return false;
}
if is_dynamic_scope_reference_key(key) {
return false;
}
if scope_kind(index, scope_id) == ScopeKind::File {
return false;
}
if is_data_context(index, scope_id) {
return false;
}
if is_param_block_scope(index, scope_id) {
return false;
}
if is_province_id_selector(key) || is_country_tag_selector(key) {
return false;
}
if event_scope_type(key).is_some() {
return false;
}
if effect_context_scope_semantics(ctx, key, scope_id, index).is_some() {
return false;
}
if is_map_group_scope_key(ctx, key, scope_id, index) {
return false;
}
if is_builtin_effect(key)
|| is_builtin_trigger(key)
|| is_builtin_scope_changer(key)
|| is_builtin_iterator(key)
|| is_builtin_special_block(key)
|| is_game_only_candidate(key)
{
return false;
}
if !is_trigger_like_scope(index, scope_id) {
return false;
}
if file_kind == ScriptFileKind::new("missions") && is_mission_slot_scope(index, scope_id) {
return false;
}
if file_kind == ScriptFileKind::new("scripted_triggers")
&& scope_kind(index, scope_id) == ScopeKind::File
{
return false;
}
true
}
fn is_effect_like_scope(index: &SemanticIndex, scope_id: usize) -> bool {
if scope_kind(index, scope_id) == ScopeKind::Trigger {
return false;
}
!is_under_trigger_scope(index, scope_id)
}
fn is_trigger_like_scope(index: &SemanticIndex, scope_id: usize) -> bool {
scope_kind(index, scope_id) == ScopeKind::Trigger || is_under_trigger_scope(index, scope_id)
}
fn allows_generic_scripted_effect_fallback(scope_kind: ScopeKind) -> bool {
matches!(
scope_kind,
ScopeKind::Effect | ScopeKind::AliasBlock | ScopeKind::Loop | ScopeKind::ScriptedEffect
)
}
fn is_under_trigger_scope(index: &SemanticIndex, mut scope_id: usize) -> bool {
loop {
let Some(scope) = index.scopes.get(scope_id) else {
return false;
};
if scope.kind == ScopeKind::Trigger {
return true;
}
let Some(parent) = scope.parent else {
return false;
};
scope_id = parent;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum EffectContextScopeSemantics {
EffectContainer,
Iterator(ScopeType),
ScopeChanger(ScopeType),
}
fn is_explicit_effect_context_scope(index: &SemanticIndex, scope_id: usize) -> bool {
is_effect_like_scope(index, scope_id)
&& allows_generic_scripted_effect_fallback(scope_kind(index, scope_id))
}
fn effect_context_scope_semantics(
ctx: &BuildContext<'_>,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> Option<EffectContextScopeSemantics> {
if !is_explicit_effect_context_scope(index, scope_id) {
return None;
}
if key == "random_list" {
return Some(EffectContextScopeSemantics::EffectContainer);
}
if matches!(
key,
"for" | "while" | "IF" | "ELSE_IF" | "else_if" | "ELSE" | "else"
) {
return Some(EffectContextScopeSemantics::EffectContainer);
}
if matches!(
key,
"every_country" | "every_subject_country" | "every_known_country" | "random_country"
) {
return Some(EffectContextScopeSemantics::Iterator(base_scope::country()));
}
if matches!(
key,
"random_owned_province" | "random_province" | "every_province"
) {
return Some(EffectContextScopeSemantics::Iterator(base_scope::province()));
}
if key == "overlord" {
return Some(EffectContextScopeSemantics::ScopeChanger(
base_scope::country(),
));
}
if let Some(selector) = numeric_effect_context_semantics(key) {
return Some(selector);
}
if is_country_tag_selector(key) {
return Some(EffectContextScopeSemantics::ScopeChanger(
base_scope::country(),
));
}
if is_map_group_scope_key(ctx, key, scope_id, index) {
return Some(EffectContextScopeSemantics::Iterator(base_scope::province()));
}
None
}
fn is_on_actions_callback_root(
file_kind: ScriptFileKind,
parent_scope_id: usize,
index: &SemanticIndex,
key: &str,
) -> bool {
file_kind == ScriptFileKind::new("on_actions")
&& scope_kind(index, parent_scope_id) == ScopeKind::File
&& key.starts_with("on_")
}
fn on_actions_callback_this_type(key: &str) -> ScopeType {
match key {
"on_adm_development" | "on_dip_development" | "on_mil_development" => {
base_scope::province()
}
_ => base_scope::country(),
}
}
fn on_actions_callback_from_type(_key: &str) -> ScopeType {
base_scope::country()
}
#[cfg(test)]
std::thread_local! {
static CREATE_CHILD_SCOPE_CWT_PATH_BINDINGS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
}
#[cfg(test)]
fn reset_create_child_scope_cwt_path_bindings() {
CREATE_CHILD_SCOPE_CWT_PATH_BINDINGS.with(|count| count.set(0));
}
#[cfg(test)]
fn take_create_child_scope_cwt_path_bindings() -> usize {
CREATE_CHILD_SCOPE_CWT_PATH_BINDINGS.with(std::cell::Cell::take)
}
fn child_scope_kind_override(
key: &str,
enclosing_conditional_context: Option<ScopeKind>,
effect_context_semantics: Option<EffectContextScopeSemantics>,
has_items: bool,
) -> Option<ScopeKind> {
if key.eq_ignore_ascii_case("if") || key.eq_ignore_ascii_case("else") {
return Some(
if matches!(
enclosing_conditional_context,
Some(ScopeKind::Effect | ScopeKind::ScriptedEffect)
) || effect_context_semantics == Some(EffectContextScopeSemantics::EffectContainer)
{
ScopeKind::Effect
} else {
ScopeKind::Trigger
},
);
}
if key.eq_ignore_ascii_case("not")
|| key.eq_ignore_ascii_case("or")
|| key.eq_ignore_ascii_case("and")
{
return Some(ScopeKind::Trigger);
}
if key == "option" {
return Some(ScopeKind::Effect);
}
(event_scope_type(key).is_some() && has_items).then_some(ScopeKind::Event)
}
fn create_child_scope(
index: &mut SemanticIndex,
parent_scope_id: usize,
ctx: &BuildContext<'_>,
key: &str,
span: &SpanRange,
items: &[AstStatement],
) -> usize {
let mut aliases = scope_aliases(index, parent_scope_id);
aliases.insert("PREV".to_string(), scope_this_type(index, parent_scope_id));
let mut this_type = scope_this_type(index, parent_scope_id);
let mut kind = ScopeKind::Block;
let enclosing_conditional_context = nearest_conditional_context_kind(index, parent_scope_id);
let effect_context_semantics = effect_context_scope_semantics(ctx, key, parent_scope_id, index);
let scope_kind_override = child_scope_kind_override(
key,
enclosing_conditional_context,
effect_context_semantics,
!items.is_empty(),
);
if is_on_actions_callback_root(ctx.file_kind.clone(), parent_scope_id, index, key) {
kind = ScopeKind::Effect;
this_type = on_actions_callback_this_type(key).into();
aliases.insert("THIS".to_string(), this_type);
aliases.insert("ROOT".to_string(), this_type);
aliases.insert(
"FROM".to_string(),
on_actions_callback_from_type(key).into(),
);
} else if key == "trigger"
|| key == "limit"
|| key == "potential"
|| key == "allow"
|| key == "condition"
|| key == "hidden_trigger"
{
kind = ScopeKind::Trigger;
} else if matches!(
key,
"effect"
| "after" | "hidden_effect"
| "immediate"
| "on_add"
| "on_remove"
| "on_start"
| "on_end"
| "on_monthly"
) {
kind = ScopeKind::Effect;
} else if let Some(semantics) = effect_context_semantics {
match semantics {
EffectContextScopeSemantics::EffectContainer => {
kind = ScopeKind::Effect;
}
EffectContextScopeSemantics::Iterator(target) => {
kind = ScopeKind::Loop;
this_type = target.into();
aliases.insert("THIS".to_string(), target.into());
}
EffectContextScopeSemantics::ScopeChanger(target) => {
kind = ScopeKind::AliasBlock;
this_type = target.into();
aliases.insert("THIS".to_string(), target.into());
}
}
} else if is_province_id_selector(key) && scope_kind(index, parent_scope_id) != ScopeKind::File
{
kind = ScopeKind::AliasBlock;
this_type = base_scope::province().into();
aliases.insert("THIS".to_string(), base_scope::province().into());
} else if is_map_group_scope_key(ctx, key, parent_scope_id, index) {
kind = ScopeKind::Loop;
this_type = base_scope::province().into();
aliases.insert("THIS".to_string(), base_scope::province().into());
} else if is_builtin_special_block(key) {
kind = special_block_scope_kind(key);
} else if is_builtin_iterator(key) {
kind = ScopeKind::Loop;
this_type = iterator_scope_type(key)
.map(MaybeScope::from)
.unwrap_or(this_type);
aliases.insert("THIS".to_string(), this_type);
} else if is_builtin_scope_changer(key) {
kind = ScopeKind::AliasBlock;
this_type = scope_changer_target_type(key)
.map(MaybeScope::from)
.unwrap_or(this_type);
aliases.insert("THIS".to_string(), this_type);
} else if key == "every_owned_province" {
kind = ScopeKind::Loop;
this_type = base_scope::province().into();
aliases.insert("THIS".to_string(), base_scope::province().into());
} else if key.eq_ignore_ascii_case("ROOT") {
kind = ScopeKind::AliasBlock;
this_type = aliases.get("ROOT").copied().unwrap_or(MaybeScope::Unknown);
aliases.insert("THIS".to_string(), this_type);
} else if key.eq_ignore_ascii_case("THIS") {
kind = ScopeKind::AliasBlock;
this_type = aliases.get("THIS").copied().unwrap_or(MaybeScope::Unknown);
aliases.insert("THIS".to_string(), this_type);
} else if key.eq_ignore_ascii_case("FROM") {
kind = ScopeKind::AliasBlock;
this_type = aliases.get("FROM").copied().unwrap_or(MaybeScope::Unknown);
aliases.insert("THIS".to_string(), this_type);
} else if key.eq_ignore_ascii_case("PREV") {
kind = ScopeKind::AliasBlock;
this_type = aliases.get("PREV").copied().unwrap_or(MaybeScope::Unknown);
aliases.insert("THIS".to_string(), this_type);
} else if is_dynamic_scope_reference_key(key) {
kind = ScopeKind::AliasBlock;
this_type = MaybeScope::Unknown;
aliases.insert("THIS".to_string(), MaybeScope::Unknown);
} else if let Some(event_this_type) = event_scope_type(key) {
kind = ScopeKind::Event;
this_type = event_this_type.into();
aliases.insert("THIS".to_string(), event_this_type.into());
aliases.insert("ROOT".to_string(), event_this_type.into());
if let Some(from_type) = event_from_type(key) {
aliases.insert("FROM".to_string(), from_type.into());
}
} else if ctx.file_kind == ScriptFileKind::new("scripted_triggers")
&& scope_kind(index, parent_scope_id) == ScopeKind::File
&& !is_keyword(key)
{
kind = ScopeKind::Trigger;
} else if ctx.file_kind == ScriptFileKind::new("scripted_effects")
&& scope_kind(index, parent_scope_id) == ScopeKind::File
&& !is_keyword(key)
{
kind = ScopeKind::ScriptedEffect;
} else if scope_kind_override.is_none()
&& let Some(file_kind_scope_kind) = ctx
.cwt_rule_engine
.and_then(|engine| {
#[cfg(test)]
CREATE_CHILD_SCOPE_CWT_PATH_BINDINGS.with(|count| count.set(count.get() + 1));
let path = scope_assignment_path(index, parent_scope_id, key);
let path_refs = path.iter().map(String::as_str).collect::<Vec<_>>();
schema_path_container_scope_kind(
engine,
ctx.file_kind.clone(),
ctx.path,
path_refs.as_slice(),
)
})
.or_else(|| {
ctx.cwt_rule_engine.map_or_else(
|| hand_container_scope_fallback(ctx.file_kind.clone(), key),
|engine| {
schema_file_kind_container_scope_kind(engine, ctx.file_kind.clone(), key)
},
)
}) {
kind = file_kind_scope_kind;
}
if let Some(scope_kind_override) = scope_kind_override {
kind = scope_kind_override;
}
push_scope(ScopeArgs {
index,
kind,
parent: Some(parent_scope_id),
this_type,
aliases,
mod_id: ctx.mod_id,
path: ctx.path,
line: span.start.line,
key,
})
}
fn scope_assignment_path(
index: &SemanticIndex,
mut parent_scope_id: usize,
key: &str,
) -> Vec<String> {
let mut path = vec![key.to_string()];
while let Some(scope) = index.scopes.get(parent_scope_id) {
if !scope.key.is_empty() {
path.push(scope.key.clone());
}
let Some(parent) = scope.parent else {
break;
};
parent_scope_id = parent;
}
path.reverse();
path
}
fn nearest_conditional_context_kind(
index: &SemanticIndex,
mut scope_id: usize,
) -> Option<ScopeKind> {
loop {
let scope = index.scopes.get(scope_id)?;
match scope.kind {
ScopeKind::Trigger | ScopeKind::Effect | ScopeKind::ScriptedEffect => {
return Some(scope.kind);
}
_ => {}
}
scope_id = scope.parent?;
}
}
fn is_map_group_scope_key(
ctx: &BuildContext<'_>,
key: &str,
scope_id: usize,
index: &SemanticIndex,
) -> bool {
if ctx.map_groups.contains(key) {
return scope_kind(index, scope_id) != ScopeKind::File;
}
if !looks_like_map_group_key(key) {
return false;
}
if scope_kind(index, scope_id) == ScopeKind::File {
return false;
}
true
}
fn province_name_table_id(path: &GamePath) -> Option<String> {
path.file_stem().map(str::to_string)
}
fn random_map_tile_id(path: &GamePath) -> Option<String> {
path.file_stem().map(str::to_string)
}
fn random_name_table_id(path: &GamePath) -> Option<String> {
match path.file_stem() {
Some("RandomLandNames") => Some("random_land_names".to_string()),
Some("RandomSeaNames") => Some("random_sea_names".to_string()),
Some("RandomLakeNames") => Some("random_lake_names".to_string()),
_ => None,
}
}
fn is_template_param_placeholder_key(key: &str) -> bool {
extract_template_param_name(key).is_some()
}
fn numeric_effect_context_semantics(key: &str) -> Option<EffectContextScopeSemantics> {
let value = key.parse::<u32>().ok()?;
if value <= 100 {
Some(EffectContextScopeSemantics::EffectContainer)
} else {
Some(EffectContextScopeSemantics::ScopeChanger(
base_scope::province(),
))
}
}
struct ScopeArgs<'a> {
index: &'a mut SemanticIndex,
kind: ScopeKind,
parent: Option<usize>,
this_type: MaybeScope,
aliases: BTreeMap<String, MaybeScope>,
mod_id: &'a str,
path: &'a GamePath,
line: usize,
key: &'a str,
}
fn push_scope(args: ScopeArgs<'_>) -> usize {
let ScopeArgs {
index,
kind,
parent,
this_type,
aliases,
mod_id,
path,
line,
key,
} = args;
let id = index.scopes.len();
index.scopes.push(ScopeNode {
id,
kind,
parent,
this_type,
aliases,
mod_id: mod_id.to_string(),
path: path.to_owned(),
span: SourceSpan { line, column: 1 },
key: key.to_string(),
});
id
}
fn scope_kind(index: &SemanticIndex, scope_id: usize) -> ScopeKind {
index
.scopes
.get(scope_id)
.map(|scope| scope.kind)
.unwrap_or(ScopeKind::Block)
}
#[allow(dead_code)]
fn scope_depth(index: &SemanticIndex, scope_id: usize) -> usize {
let mut depth = 0;
let mut current = scope_id;
while let Some(scope) = index.scopes.get(current) {
if let Some(parent) = scope.parent {
depth += 1;
current = parent;
} else {
break;
}
}
depth
}
fn is_mission_slot_definition(file_kind: ScriptFileKind, items: &[AstStatement]) -> bool {
if file_kind != ScriptFileKind::new("missions") {
return false;
}
items.iter().any(|stmt| {
if let AstStatement::Assignment { key, .. } = stmt {
matches!(
key.as_str(),
"icon"
| "position" | "required_missions"
| "trigger" | "effect"
| "ai_weight" | "provinces_to_highlight"
| "completed_by"
| "slot" | "generic" | "has_country_shield"
)
} else {
false
}
})
}
fn is_mission_slot_scope(index: &SemanticIndex, scope_id: usize) -> bool {
let Some(scope) = index.scopes.get(scope_id) else {
return false;
};
if scope.kind != ScopeKind::Block {
return false;
}
let Some(parent_id) = scope.parent else {
return false;
};
let Some(parent) = index.scopes.get(parent_id) else {
return false;
};
matches!(parent.kind, ScopeKind::Block | ScopeKind::File)
}
fn is_param_block_scope(index: &SemanticIndex, scope_id: usize) -> bool {
let Some(scope) = index.scopes.get(scope_id) else {
return false;
};
if scope.kind != ScopeKind::Block {
return false;
}
let mut cursor = scope.parent;
while let Some(parent_id) = cursor {
let Some(parent) = index.scopes.get(parent_id) else {
return false;
};
match parent.kind {
ScopeKind::Effect
| ScopeKind::Trigger
| ScopeKind::Loop
| ScopeKind::AliasBlock
| ScopeKind::ScriptedEffect => return true,
ScopeKind::Block => {
cursor = parent.parent;
}
_ => return false,
}
}
false
}
fn is_data_context(index: &SemanticIndex, scope_id: usize) -> bool {
const DATA_CONTEXT_KEYS: &[&str] = &[
"check_variable",
"set_variable",
"change_variable",
"multiply_variable",
"divide_variable",
"subtract_variable",
"export_to_variable",
"is_variable_equal",
"had_recent_war",
"required_missions",
"ME_has_mil_advisor",
"ME_has_dip_advisor",
"ME_has_adm_advisor",
"define",
"defines",
"who",
"export_to_variable",
"country_event_with_option_insight",
"complex_dynamic_effect",
"create_general_with_pips",
"ME_create_flagship",
"ME_legitimacy_or_tribal_allegiance_trigger",
"NED_faction_is_superior_by",
"calc_true_if",
];
let mut current = scope_id;
loop {
let Some(scope) = index.scopes.get(current) else {
return false;
};
if DATA_CONTEXT_KEYS.contains(&scope.key.as_str()) {
return true;
}
let Some(parent) = scope.parent else {
return false;
};
current = parent;
}
}
fn scope_this_type(index: &SemanticIndex, scope_id: usize) -> MaybeScope {
index
.scopes
.get(scope_id)
.map(|scope| scope.this_type)
.unwrap_or(MaybeScope::Unknown)
}
fn scope_aliases(index: &SemanticIndex, scope_id: usize) -> BTreeMap<String, MaybeScope> {
index
.scopes
.get(scope_id)
.map(|scope| scope.aliases.clone())
.unwrap_or_default()
}
fn line_from_stmt(stmt: Option<&AstStatement>) -> usize {
stmt.map(|item| match item {
AstStatement::Assignment { span, .. } => span.start.line,
AstStatement::Item { span, .. } => span.start.line,
AstStatement::Comment { span, .. } => span.start.line,
})
.unwrap_or(1)
}
fn find_scripted_effect_definition(
index: &SemanticIndex,
mod_id: &str,
path: &GamePath,
name: &str,
) -> Option<usize> {
index.definitions.iter().position(|item| {
item.kind == SymbolKind::ScriptedEffect
&& item.mod_id == mod_id
&& item.path == path
&& item.local_name == name
})
}
fn is_event_call(key: &str, value: &AstValue) -> bool {
event_scope_type(key).is_some() && matches!(value, AstValue::Block { .. })
}
fn extract_event_call_id(value: &AstValue) -> Option<String> {
let AstValue::Block { items, .. } = value else {
return None;
};
extract_assignment_scalar(items, "id")
}
fn extract_assignment_scalar(items: &[AstStatement], name: &str) -> Option<String> {
for item in items {
if let AstStatement::Assignment { key, value, .. } = item
&& key == name
&& let Some(text) = scalar_text(value)
{
return Some(text);
}
}
None
}
fn scalar_text(value: &AstValue) -> Option<String> {
let AstValue::Scalar { value, .. } = value else {
return None;
};
Some(value.as_text())
}
fn collect_required_params(items: &[AstStatement]) -> Vec<String> {
let param_re = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)\$").expect("valid param regex");
let mut params = Vec::new();
for stmt in items {
match stmt {
AstStatement::Assignment { value, .. } | AstStatement::Item { value, .. } => {
collect_params_from_value(value, ¶m_re, &mut params)
}
AstStatement::Comment { .. } => {}
}
}
params
}
fn collect_optional_params_from_source(
source: &str,
start_offset: usize,
end_offset: usize,
) -> Vec<String> {
if source.is_empty() || end_offset <= start_offset || end_offset > source.len() {
return Vec::new();
}
let slice = &source[start_offset..end_offset];
let bracket_re =
Regex::new(r"\[\[\s*([A-Za-z_][A-Za-z0-9_]*)\s*\]").expect("valid optional-block regex");
let default_re =
Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)\s*\|").expect("valid default-fallback regex");
let mut names: HashSet<String> = HashSet::new();
for cap in bracket_re.captures_iter(slice) {
if let Some(name) = cap.get(1) {
names.insert(name.as_str().to_string());
}
}
for cap in default_re.captures_iter(slice) {
if let Some(name) = cap.get(1) {
names.insert(name.as_str().to_string());
}
}
let mut collected: Vec<String> = names.into_iter().collect();
collected.sort();
collected
}
fn collect_params_from_value(value: &AstValue, re: &Regex, out: &mut Vec<String>) {
match value {
AstValue::Scalar { value, .. } => {
let text = value.as_text();
for cap in re.captures_iter(&text) {
if let Some(param) = cap.get(1) {
out.push(param.as_str().to_string());
}
}
}
AstValue::Block { items, .. } => {
for stmt in items {
match stmt {
AstStatement::Assignment { value, .. } | AstStatement::Item { value, .. } => {
collect_params_from_value(value, re, out)
}
AstStatement::Comment { .. } => {}
}
}
}
}
}
#[derive(Default)]
struct ProvidedParams {
names: Vec<String>,
bindings: Vec<ParamBinding>,
}
fn collect_provided_params(local_name: &str, items: &[AstStatement]) -> ProvidedParams {
let mut params = ProvidedParams::default();
let contract_names = explicit_contract_param_names(local_name);
for stmt in items {
if let AstStatement::Assignment { key, value, .. } = stmt {
let is_explicit_param_name = key
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch == '_' || ch.is_ascii_digit())
|| contract_names.contains(key.as_str());
let is_named_param_binding = !key.is_empty()
&& key
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
if is_explicit_param_name || is_named_param_binding {
params.names.push(key.clone());
}
if is_named_param_binding && let Some(value) = scalar_text(value) {
params.bindings.push(ParamBinding {
name: key.clone(),
value,
});
}
}
}
params
}
fn collect_scope_param_names(items: &[AstStatement]) -> Vec<String> {
let mut names = HashSet::new();
collect_scope_param_names_from_statements(items, &mut names);
let mut collected: Vec<String> = names.into_iter().collect();
collected.sort();
collected
}
fn extract_template_param_name(value: &str) -> Option<&str> {
let trimmed = value.trim();
if !(trimmed.starts_with('$') && trimmed.ends_with('$') && trimmed.len() > 2) {
return None;
}
let param = &trimmed[1..trimmed.len() - 1];
if param
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
{
Some(param)
} else {
None
}
}
fn collect_scope_param_names_from_statements(items: &[AstStatement], names: &mut HashSet<String>) {
for stmt in items {
if let AstStatement::Assignment { key, value, .. } = stmt {
if let Some(param_name) = extract_template_param_name(key)
&& matches!(value, AstValue::Block { .. })
{
names.insert(param_name.to_string());
}
if let AstValue::Block { items, .. } = value {
collect_scope_param_names_from_statements(items, names);
}
}
}
}
fn is_alias_key(key: &str) -> bool {
matches!(key, "ROOT" | "FROM" | "THIS" | "PREV")
}
pub fn is_decision_container_key(key: &str) -> bool {
matches!(
key,
"country_decisions" | "province_decisions" | "religion_decisions" | "government_decisions"
)
}
fn is_keyword(key: &str) -> bool {
if is_reserved_keyword(key) || is_contextual_keyword(key) {
return true;
}
matches!(key, "condition" | "from")
}
fn looks_like_ui_container_key(key: &str) -> bool {
key.ends_with("Type")
|| matches!(
key,
"containerWindow" | "window" | "button" | "icon" | "sprite" | "shield"
)
}
fn is_ui_identifier_candidate(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& value
.chars()
.all(|ch| matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '.' | '-' | ':'))
}
fn is_ui_resource_key(key: &str) -> bool {
matches!(
key,
"spriteType"
| "buttonSprite"
| "quadTextureSprite"
| "texturefile"
| "icon" | "iconType"
| "pdxmesh"
| "mesh"
)
}
fn is_inferred_callable_kind(kind: SymbolKind) -> bool {
matches!(
kind,
SymbolKind::ScriptedEffect | SymbolKind::ScriptedTrigger
)
}
fn resolve_reference_targets_for_kind(
index: &SemanticIndex,
reference: &SymbolReference,
kind: SymbolKind,
) -> Vec<usize> {
if reference.kind != kind {
return Vec::new();
}
let mut exact = Vec::new();
for (idx, def) in index.definitions.iter().enumerate() {
if def.kind != kind {
continue;
}
if def.module == reference.module && def.local_name == reference.name {
exact.push(idx);
}
}
if !exact.is_empty() {
return exact;
}
let mut by_local = Vec::new();
for (idx, def) in index.definitions.iter().enumerate() {
if def.kind != kind {
continue;
}
if def.local_name == reference.name {
by_local.push(idx);
}
}
if !by_local.is_empty() {
return by_local;
}
Vec::new()
}
pub fn resolve_scripted_effect_reference_targets(
index: &SemanticIndex,
reference: &SymbolReference,
) -> Vec<usize> {
resolve_reference_targets_for_kind(index, reference, SymbolKind::ScriptedEffect)
}
pub fn resolve_scripted_trigger_reference_targets(
index: &SemanticIndex,
reference: &SymbolReference,
) -> Vec<usize> {
resolve_reference_targets_for_kind(index, reference, SymbolKind::ScriptedTrigger)
}
pub fn resolve_symbol_reference_targets(
index: &SemanticIndex,
reference: &SymbolReference,
) -> Vec<usize> {
match reference.kind {
SymbolKind::Event => resolve_event_reference_targets(index, reference),
SymbolKind::ScriptedEffect => {
let targets = resolve_scripted_effect_reference_targets(index, reference);
if targets.is_empty() {
resolve_cross_kind_reference_targets(index, reference, SymbolKind::ScriptedTrigger)
} else {
targets
}
}
SymbolKind::ScriptedTrigger => {
let targets = resolve_scripted_trigger_reference_targets(index, reference);
if targets.is_empty() {
resolve_cross_kind_reference_targets(index, reference, SymbolKind::ScriptedEffect)
} else {
targets
}
}
_ => Vec::new(),
}
}
pub fn count_symbol_references_resolving_to_mod(
index: &SemanticIndex,
source_mod_id: &str,
target_mod_id: &str,
) -> u32 {
let count = index
.references
.iter()
.filter(|reference| reference.mod_id == source_mod_id)
.filter(|reference| {
resolve_symbol_reference_targets(index, reference)
.into_iter()
.filter_map(|target| index.definitions.get(target))
.any(|definition| definition.mod_id == target_mod_id)
})
.count();
count.min(u32::MAX as usize) as u32
}
pub fn resolve_cross_kind_reference_targets(
index: &SemanticIndex,
reference: &SymbolReference,
target_kind: SymbolKind,
) -> Vec<usize> {
let mut by_local = Vec::new();
for (idx, def) in index.definitions.iter().enumerate() {
if def.kind != target_kind {
continue;
}
if def.module == reference.module && def.local_name == reference.name {
return vec![idx];
}
if def.local_name == reference.name {
by_local.push(idx);
}
}
by_local
}
pub fn resolve_event_reference_targets(
index: &SemanticIndex,
reference: &SymbolReference,
) -> Vec<usize> {
if reference.kind != SymbolKind::Event {
return Vec::new();
}
let ref_name = reference.name.as_str();
let mut matches = Vec::new();
for (idx, def) in index.definitions.iter().enumerate() {
if def.kind != SymbolKind::Event {
continue;
}
let def_name = def.name.as_str();
if def_name == ref_name {
matches.push(idx);
continue;
}
if def_name.ends_with(ref_name)
&& def_name.as_bytes().get(def_name.len() - ref_name.len() - 1) == Some(&b'.')
{
matches.push(idx);
continue;
}
if ref_name.ends_with(def_name)
&& ref_name.as_bytes().get(ref_name.len() - def_name.len() - 1) == Some(&b'.')
{
matches.push(idx);
}
}
matches
}
fn infer_definition_scope_from_references(index: &mut SemanticIndex) {
let callable_scope_map = build_inferred_callable_scope_map(index);
let mut observed_masks: Vec<ScopeSet> = index
.definitions
.iter()
.map(|definition| {
if is_inferred_callable_kind(definition.kind) {
scope_mask(definition.declared_this_type)
} else {
ScopeSet::EMPTY
}
})
.collect();
let mut changed = true;
while changed {
changed = false;
for reference in &index.references {
if !is_inferred_callable_kind(reference.kind) {
continue;
}
let caller_mask = effective_scope_mask_with_overrides(
index,
&callable_scope_map,
&observed_masks,
reference.scope_id,
);
if caller_mask.is_empty() {
continue;
}
let target_defs = match reference.kind {
SymbolKind::ScriptedEffect => {
resolve_scripted_effect_reference_targets(index, reference)
}
SymbolKind::ScriptedTrigger => {
resolve_scripted_trigger_reference_targets(index, reference)
}
_ => Vec::new(),
};
for def_idx in target_defs {
let mut merged = observed_masks[def_idx] | caller_mask;
if let Some(definition) = index.definitions.get(def_idx)
&& definition.kind == SymbolKind::ScriptedEffect
{
for binding in &reference.param_bindings {
if !definition
.scope_param_names
.iter()
.any(|name| name == &binding.name)
{
continue;
}
merged |= binding_value_scope_mask(
index,
&callable_scope_map,
&observed_masks,
reference.scope_id,
&binding.value,
);
}
}
if merged != observed_masks[def_idx] {
observed_masks[def_idx] = merged;
changed = true;
}
}
}
}
let backfill_candidates: Vec<bool> = index
.definitions
.iter()
.enumerate()
.map(|(idx, definition)| {
definition.kind == SymbolKind::ScriptedEffect && observed_masks[idx].is_empty()
})
.collect();
for scope_id in 0..index.scopes.len() {
let scope_mask = effective_scope_mask_with_overrides(
index,
&callable_scope_map,
&observed_masks,
scope_id,
);
if scope_mask.is_empty() {
continue;
}
let Some(def_idx) = nearest_enclosing_scripted_effect_definition_index(
index,
&callable_scope_map,
scope_id,
) else {
continue;
};
if !backfill_candidates[def_idx] {
continue;
}
observed_masks[def_idx] |= scope_mask;
}
for usage in &index.key_usages {
if usage.key != "capital_scope" {
continue;
}
let Some(def_idx) =
nearest_enclosing_callable_definition_index(index, &callable_scope_map, usage.scope_id)
else {
continue;
};
let Some(definition) = index.definitions.get(def_idx) else {
continue;
};
if definition.kind != SymbolKind::ScriptedTrigger {
continue;
}
observed_masks[def_idx] |= scope_mask(base_scope::country());
}
for (idx, definition) in index.definitions.iter_mut().enumerate() {
if !is_inferred_callable_kind(definition.kind) {
continue;
}
definition.inferred_this_mask = observed_masks[idx];
definition.inferred_this_type = scope_type_from_mask(observed_masks[idx]);
}
}
fn infer_definition_dynamic_alias_masks_from_references(index: &mut SemanticIndex) {
let callable_scope_map = build_inferred_callable_scope_map(index);
let mut from_masks: Vec<ScopeSet> = vec![ScopeSet::EMPTY; index.definitions.len()];
let mut root_masks: Vec<ScopeSet> = vec![ScopeSet::EMPTY; index.definitions.len()];
let mut changed = true;
while changed {
changed = false;
for reference in &index.references {
if !matches!(
reference.kind,
SymbolKind::ScriptedEffect | SymbolKind::ScriptedTrigger | SymbolKind::Event
) {
continue;
}
let caller_from = caller_alias_mask_via_chain(
index,
&callable_scope_map,
&from_masks,
reference.scope_id,
"FROM",
);
let caller_root = caller_alias_mask_via_chain(
index,
&callable_scope_map,
&root_masks,
reference.scope_id,
"ROOT",
);
if caller_from.is_empty() && caller_root.is_empty() {
continue;
}
let target_defs = match reference.kind {
SymbolKind::ScriptedEffect => {
resolve_scripted_effect_reference_targets(index, reference)
}
SymbolKind::ScriptedTrigger => {
resolve_scripted_trigger_reference_targets(index, reference)
}
SymbolKind::Event => resolve_event_reference_targets(index, reference),
_ => Vec::new(),
};
for def_idx in target_defs {
let merged_from = from_masks[def_idx] | caller_from;
if merged_from != from_masks[def_idx] {
from_masks[def_idx] = merged_from;
changed = true;
}
let merged_root = root_masks[def_idx] | caller_root;
if merged_root != root_masks[def_idx] {
root_masks[def_idx] = merged_root;
changed = true;
}
}
}
}
let scope_updates: Vec<(usize, Option<MaybeScope>, Option<MaybeScope>)> = index
.definitions
.iter()
.enumerate()
.filter_map(|(idx, def)| {
if !matches!(
def.kind,
SymbolKind::ScriptedEffect | SymbolKind::ScriptedTrigger | SymbolKind::Event
) {
return None;
}
let from_ty =
(!from_masks[idx].is_empty()).then(|| scope_type_from_mask(from_masks[idx]));
let root_ty =
(!root_masks[idx].is_empty()).then(|| scope_type_from_mask(root_masks[idx]));
if from_ty.is_none() && root_ty.is_none() {
return None;
}
Some((def.scope_id, from_ty, root_ty))
})
.collect();
for (scope_id, from_ty, root_ty) in scope_updates {
if let Some(scope) = index.scopes.get_mut(scope_id) {
if let Some(ty) = from_ty {
scope.aliases.entry("FROM".to_string()).or_insert(ty);
}
if let Some(ty) = root_ty {
scope.aliases.entry("ROOT".to_string()).or_insert(ty);
}
}
}
for (idx, definition) in index.definitions.iter_mut().enumerate() {
if matches!(
definition.kind,
SymbolKind::ScriptedEffect | SymbolKind::ScriptedTrigger | SymbolKind::Event
) {
definition.inferred_from_mask = from_masks[idx];
definition.inferred_root_mask = root_masks[idx];
}
}
}
fn caller_alias_mask_via_chain(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
observed_alias_masks: &[ScopeSet],
mut scope_id: usize,
alias: &str,
) -> ScopeSet {
loop {
let Some(scope) = index.scopes.get(scope_id) else {
return ScopeSet::EMPTY;
};
if let Some(ty) = scope.aliases.get(alias) {
let mask = scope_mask(*ty);
if !mask.is_empty() {
return mask;
}
}
if let Some(def_idx) = callable_scope_map.get(&scope_id)
&& let Some(mask) = observed_alias_masks.get(*def_idx).copied()
&& !mask.is_empty()
{
return mask;
}
let Some(parent) = scope.parent else {
return ScopeSet::EMPTY;
};
scope_id = parent;
}
}
pub(crate) fn build_inferred_callable_scope_map(index: &SemanticIndex) -> HashMap<usize, usize> {
index
.definitions
.iter()
.enumerate()
.filter_map(|(idx, definition)| {
is_inferred_callable_kind(definition.kind).then_some((definition.scope_id, idx))
})
.collect()
}
pub(crate) fn collect_inferred_callable_masks(index: &SemanticIndex) -> Vec<ScopeSet> {
index
.definitions
.iter()
.map(|definition| definition.inferred_this_mask)
.collect()
}
fn binding_value_scope_mask(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
observed_masks: &[ScopeSet],
scope_id: usize,
value: &str,
) -> ScopeSet {
let trimmed = value.trim();
if trimmed.is_empty() {
return ScopeSet::EMPTY;
}
match trimmed {
"THIS" | "ROOT" | "FROM" | "PREV" => {
return effective_alias_scope_mask_with_overrides(
index,
callable_scope_map,
observed_masks,
scope_id,
trimmed,
);
}
"owner" => return scope_mask(base_scope::country()),
"capital_scope" => return scope_mask(base_scope::province()),
_ => {}
}
if is_country_tag_selector(trimmed) {
return scope_mask(base_scope::country());
}
if is_province_id_selector(trimmed) {
return scope_mask(base_scope::province());
}
ScopeSet::EMPTY
}
pub(crate) fn effective_alias_scope_mask_with_overrides(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
observed_masks: &[ScopeSet],
mut scope_id: usize,
alias: &str,
) -> ScopeSet {
let mut fallback_mask = ScopeSet::EMPTY;
loop {
let Some(scope) = index.scopes.get(scope_id) else {
return fallback_mask;
};
if alias == "THIS" {
let this_mask = scope_mask(scope.this_type);
if !this_mask.is_empty() {
return this_mask;
}
if let Some(def_idx) = callable_scope_map.get(&scope_id) {
let inferred_mask = observed_masks
.get(*def_idx)
.copied()
.unwrap_or(ScopeSet::EMPTY);
if !inferred_mask.is_empty() {
return inferred_mask;
}
}
}
if let Some(alias_type) = scope.aliases.get(alias) {
let alias_mask = scope_mask(*alias_type);
if !alias_mask.is_empty() {
return alias_mask;
}
}
if fallback_mask.is_empty()
&& let Some(def_idx) = callable_scope_map.get(&scope_id)
{
let inferred_mask = observed_masks
.get(*def_idx)
.copied()
.unwrap_or(ScopeSet::EMPTY);
if !inferred_mask.is_empty() {
fallback_mask = inferred_mask;
}
}
let Some(parent) = scope.parent else {
return fallback_mask;
};
scope_id = parent;
}
}
fn nearest_enclosing_callable_definition_index(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
mut scope_id: usize,
) -> Option<usize> {
loop {
if let Some(def_idx) = callable_scope_map.get(&scope_id)
&& index
.definitions
.get(*def_idx)
.is_some_and(|definition| is_inferred_callable_kind(definition.kind))
{
return Some(*def_idx);
}
let parent = index.scopes.get(scope_id).and_then(|scope| scope.parent)?;
scope_id = parent;
}
}
fn nearest_enclosing_scripted_effect_definition_index(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
scope_id: usize,
) -> Option<usize> {
let def_idx = nearest_enclosing_callable_definition_index(index, callable_scope_map, scope_id)?;
index
.definitions
.get(def_idx)
.filter(|definition| definition.kind == SymbolKind::ScriptedEffect)
.map(|_| def_idx)
}
fn scope_mask<T>(scope: T) -> ScopeSet
where
T: Into<ScopeSet>,
{
scope.into()
}
fn scope_type_from_mask(mask: ScopeSet) -> MaybeScope {
mask.as_single_scope()
.map(MaybeScope::Known)
.unwrap_or(MaybeScope::Unknown)
}
pub(crate) fn effective_scope_mask_with_overrides(
index: &SemanticIndex,
callable_scope_map: &HashMap<usize, usize>,
observed_masks: &[ScopeSet],
mut scope_id: usize,
) -> ScopeSet {
loop {
let Some(scope) = index.scopes.get(scope_id) else {
return ScopeSet::EMPTY;
};
if !scope.this_type.is_unknown() {
return scope_mask(scope.this_type);
}
if let Some(def_idx) = callable_scope_map.get(&scope_id)
&& let Some(inferred_mask) = observed_masks.get(*def_idx).copied()
&& !inferred_mask.is_empty()
{
return inferred_mask;
}
let Some(parent) = scope.parent else {
return ScopeSet::EMPTY;
};
scope_id = parent;
}
}
#[cfg(test)]
mod tests;