foch 0.1.0

Paradox mod static analysis toolkit with CLI and EU4-focused language tooling
Documentation
use crate::check::model::{GraphFormat, ScopeKind, ScopeType, SemanticIndex, SymbolKind};
use std::collections::HashMap;

pub fn export_graph(index: &SemanticIndex, format: GraphFormat) -> String {
	match format {
		GraphFormat::Json => serde_json::to_string_pretty(index).unwrap_or_else(|err| {
			format!(
				"{{\"error\":\"graph json export failed: {}\"}}",
				escape_json(err.to_string())
			)
		}),
		GraphFormat::Dot => export_dot(index),
	}
}

fn export_dot(index: &SemanticIndex) -> String {
	let mut lines = Vec::new();
	lines.push("digraph foch_semantic {".to_string());
	lines.push("\trankdir=LR;".to_string());
	lines.push("\tnode [fontname=\"monospace\"];".to_string());

	for scope in &index.scopes {
		let label = format!(
			"scope:{}\\n{}\\nTHIS={}\\n{}:{}",
			scope.id,
			scope_kind_text(scope.kind),
			scope_type_text(scope.this_type),
			scope.path.display(),
			scope.span.line
		);
		lines.push(format!(
			"\tscope_{} [shape=oval,label=\"{}\"];",
			scope.id,
			escape_dot(&label)
		));
		if let Some(parent) = scope.parent {
			lines.push(format!("\tscope_{} -> scope_{};", parent, scope.id));
		}
	}

	for (idx, def) in index.definitions.iter().enumerate() {
		let label = format!(
			"def:{}\\n{}\\n{}\\n{}:{}",
			idx,
			symbol_kind_text(def.kind),
			def.name,
			def.path.display(),
			def.line
		);
		lines.push(format!(
			"\tdef_{} [shape=box,style=filled,fillcolor=\"palegreen\",label=\"{}\"];",
			idx,
			escape_dot(&label)
		));
		lines.push(format!("\tscope_{} -> def_{};", def.scope_id, idx));
	}

	let mut def_lookup: HashMap<(SymbolKind, String), usize> = HashMap::new();
	for (idx, def) in index.definitions.iter().enumerate() {
		def_lookup.insert((def.kind, def.name.clone()), idx);
	}

	for (idx, reference) in index.references.iter().enumerate() {
		let label = format!(
			"ref:{}\\n{}\\n{}\\n{}:{}",
			idx,
			symbol_kind_text(reference.kind),
			reference.name,
			reference.path.display(),
			reference.line
		);
		lines.push(format!(
			"\tref_{} [shape=note,style=filled,fillcolor=\"lightblue\",label=\"{}\"];",
			idx,
			escape_dot(&label)
		));
		lines.push(format!("\tscope_{} -> ref_{};", reference.scope_id, idx));
		if let Some(def_idx) = def_lookup.get(&(reference.kind, reference.name.clone())) {
			lines.push(format!(
				"\tref_{} -> def_{} [style=dashed,color=gray40];",
				idx, def_idx
			));
		}
	}

	lines.push("}".to_string());
	lines.join("\n")
}

fn scope_kind_text(kind: ScopeKind) -> &'static str {
	match kind {
		ScopeKind::File => "File",
		ScopeKind::Event => "Event",
		ScopeKind::Decision => "Decision",
		ScopeKind::ScriptedEffect => "ScriptedEffect",
		ScopeKind::Trigger => "Trigger",
		ScopeKind::Effect => "Effect",
		ScopeKind::Loop => "Loop",
		ScopeKind::AliasBlock => "AliasBlock",
		ScopeKind::Block => "Block",
	}
}

fn scope_type_text(scope_type: ScopeType) -> &'static str {
	match scope_type {
		ScopeType::Country => "Country",
		ScopeType::Province => "Province",
		ScopeType::Unknown => "Unknown",
	}
}

fn symbol_kind_text(kind: SymbolKind) -> &'static str {
	match kind {
		SymbolKind::ScriptedEffect => "scripted_effect",
		SymbolKind::Event => "event",
		SymbolKind::Decision => "decision",
		SymbolKind::DiplomaticAction => "diplomatic_action",
		SymbolKind::TriggeredModifier => "triggered_modifier",
	}
}

fn escape_dot(value: &str) -> String {
	value.replace('\\', "\\\\").replace('"', "\\\"")
}

fn escape_json(value: String) -> String {
	value
		.replace('\\', "\\\\")
		.replace('"', "\\\"")
		.replace('\n', "\\n")
}