use std::collections::{BTreeMap, BTreeSet};
use crate::ast::token::{Token, TokenBlock, TokenLiteral, TokenType, TokenValue};
use crate::diagnostics::Diagnostic;
use super::types::{ResolvedToken, ResolvedValue, TokenResolution};
use super::validate::{type_name_of, validate_literal};
pub fn resolve_tokens(block: &TokenBlock) -> TokenResolution {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut resolved: BTreeMap<String, ResolvedToken> = BTreeMap::new();
let mut index: BTreeMap<&str, &Token> = BTreeMap::new();
let mut seen_ids: BTreeSet<&str> = BTreeSet::new();
for token in &block.tokens {
if seen_ids.contains(token.id.as_str()) {
diagnostics.push(Diagnostic::error(
"token.duplicate_id",
format!(
"token '{}' is defined more than once; the second definition is ignored",
token.id
),
token.source_span,
Some(token.id.clone()),
));
} else {
seen_ids.insert(token.id.as_str());
index.insert(token.id.as_str(), token);
}
}
for token in &block.tokens {
let Some(canonical) = index.get(token.id.as_str()) else {
continue;
};
if !std::ptr::eq(*canonical, token) {
continue;
}
if let TokenType::Unknown(ref type_name) = token.token_type {
diagnostics.push(Diagnostic::warning(
"token.unknown_type",
format!(
"token '{}' has unrecognized type '{}' (version-relative; \
this type may be valid in a later schema version)",
token.id, type_name
),
token.source_span,
Some(token.id.clone()),
));
continue;
}
match resolve_token_to_literal(token, &index, &mut diagnostics) {
Some((literal, resolved_type)) => {
if resolved_type != token.token_type {
diagnostics.push(Diagnostic::error(
"token.type_mismatch",
format!(
"token '{}' has declared type '{}' but its alias \
chain resolves to a token of type '{}'",
token.id,
type_name_of(&token.token_type),
type_name_of(&resolved_type),
),
token.source_span,
Some(token.id.clone()),
));
continue;
}
match validate_literal(
&token.id,
&token.token_type,
&literal,
token.source_span,
&mut diagnostics,
) {
Some(rv) => {
resolved.insert(
token.id.clone(),
ResolvedToken {
token_type: token.token_type.clone(),
value: rv,
},
);
}
None => {
}
}
}
None => {
}
}
}
let gradient_stops: Vec<(String, Vec<String>)> = resolved
.iter()
.filter_map(|(id, rt)| match &rt.value {
ResolvedValue::Gradient(g) => Some((
id.clone(),
g.stops
.iter()
.map(|(_, color_id)| color_id.clone())
.collect(),
)),
ResolvedValue::Color(_)
| ResolvedValue::CmykColor { .. }
| ResolvedValue::Dimension(_)
| ResolvedValue::Number(_)
| ResolvedValue::FontFamily(_)
| ResolvedValue::FontWeight(_)
| ResolvedValue::Shadow(_)
| ResolvedValue::Filter(_)
| ResolvedValue::Mask(_) => None,
})
.collect();
for (id, stop_color_ids) in &gradient_stops {
let span = index.get(id.as_str()).and_then(|t| t.source_span);
for color_token_id in stop_color_ids {
match resolved.get(color_token_id.as_str()) {
None => diagnostics.push(Diagnostic::error(
"gradient.stop_unresolved",
format!(
"gradient '{}' stop references unknown token '{}' \
— declare the color token in the `tokens` block or fix the id",
id, color_token_id
),
span,
Some(id.clone()),
)),
Some(rt) if rt.value.as_color_hex().is_none() => {
diagnostics.push(Diagnostic::error(
"gradient.stop_wrong_type",
format!(
"gradient '{}' stop references token '{}' of type '{}' \
but a color token is required",
id,
color_token_id,
type_name_of(&rt.token_type),
),
span,
Some(id.clone()),
));
}
Some(_) => {}
}
}
}
let shadow_layers: Vec<(String, Vec<String>)> = resolved
.iter()
.filter_map(|(id, rt)| match &rt.value {
ResolvedValue::Shadow(s) => Some((
id.clone(),
s.layers
.iter()
.map(|layer| layer.color_token.clone())
.collect(),
)),
ResolvedValue::Color(_)
| ResolvedValue::CmykColor { .. }
| ResolvedValue::Dimension(_)
| ResolvedValue::Number(_)
| ResolvedValue::FontFamily(_)
| ResolvedValue::FontWeight(_)
| ResolvedValue::Gradient(_)
| ResolvedValue::Filter(_)
| ResolvedValue::Mask(_) => None,
})
.collect();
for (id, layer_color_ids) in &shadow_layers {
let span = index.get(id.as_str()).and_then(|t| t.source_span);
for color_token_id in layer_color_ids {
match resolved.get(color_token_id.as_str()) {
None => diagnostics.push(Diagnostic::error(
"shadow.layer_unresolved",
format!(
"shadow '{}' layer references unknown token '{}'",
id, color_token_id
),
span,
Some(id.clone()),
)),
Some(rt) if rt.value.as_color_hex().is_none() => {
diagnostics.push(Diagnostic::error(
"shadow.layer_wrong_type",
format!(
"shadow '{}' layer references token '{}' of type '{}' \
but a color token is required",
id,
color_token_id,
type_name_of(&rt.token_type),
),
span,
Some(id.clone()),
));
}
Some(_) => {}
}
}
}
TokenResolution {
resolved,
diagnostics,
}
}
fn resolve_token_to_literal<'a>(
start: &'a Token,
index: &BTreeMap<&str, &'a Token>,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(TokenLiteral, TokenType)> {
let mut visited: BTreeSet<&str> = BTreeSet::new();
let mut current: &Token = start;
loop {
match ¤t.value {
TokenValue::Literal(lit) => {
return Some((lit.clone(), current.token_type.clone()));
}
TokenValue::Reference { token_id } => {
if visited.contains(token_id.as_str()) {
diagnostics.push(Diagnostic::error(
"token.cyclic_reference",
format!(
"token '{}' participates in a cyclic alias chain \
(cycle detected at '{}')",
start.id, token_id
),
start.source_span,
Some(start.id.clone()),
));
return None;
}
if token_id == ¤t.id {
diagnostics.push(Diagnostic::error(
"token.cyclic_reference",
format!("token '{}' references itself", current.id),
current.source_span,
Some(current.id.clone()),
));
return None;
}
visited.insert(current.id.as_str());
match index.get(token_id.as_str()) {
Some(next) => {
current = next;
}
None => {
diagnostics.push(Diagnostic::error(
"token.unknown_reference",
format!(
"token '{}' references '{}' which does not exist \
— check the spelling and that the token is declared in the `tokens` block",
start.id, token_id
),
start.source_span,
Some(start.id.clone()),
));
return None;
}
}
}
}
}
}