use std::collections::{HashMap, HashSet};
use crate::build::LangBundle;
use crate::build::typed::{
Annotation, ElementKind, Id, Message, Ref, RefKind, VarType, annotation,
};
pub struct Lints {
pub mistakes: Vec<String>,
pub ineffective: Vec<String>,
pub untyped: Vec<String>,
}
pub fn check(langs: &[LangBundle], default: &LangBundle, common: &HashSet<Id>) -> Lints {
let mut refs_by_message: HashMap<&str, Vec<&Ref>> = HashMap::new();
for m in &default.messages {
refs_by_message
.entry(m.id.message.as_str())
.or_default()
.extend(&m.pattern_refs);
}
let mut mistakes = Vec::new();
for msg in &default.messages {
mistakes.extend(comment_mistakes(msg, &refs_by_message));
}
mistakes.extend(detached_comments(default));
mistakes.sort();
let mut ineffective = Vec::new();
for lang in langs {
if lang.language_id != default.language_id {
ineffective.extend(ineffective_annotations(lang));
}
}
ineffective.sort();
let element_vars: HashSet<(&str, &str)> = default
.messages
.iter()
.flat_map(|m| {
m.elements
.iter()
.filter(|e| e.kind == ElementKind::Variable)
.map(move |e| (m.id.message.as_str(), e.name.as_str()))
})
.collect();
let mut untyped = Vec::new();
for msg in &default.messages {
if common.contains(&msg.id) {
untyped.extend(untyped_variables(msg, &element_vars));
}
}
untyped.sort();
Lints {
mistakes,
ineffective,
untyped,
}
}
fn comment_mistakes(msg: &Message, refs_by_message: &HashMap<&str, Vec<&Ref>>) -> Vec<String> {
if msg.comment.is_empty() {
return Vec::new();
}
let no_refs = Vec::new();
let refs = refs_by_message
.get(msg.id.message.as_str())
.unwrap_or(&no_refs);
let mut out = Vec::new();
for (i, line) in msg.comment.iter().enumerate() {
let Some(a) = annotation(line) else { continue };
let at = format!("{}:{}", msg.file, msg.comment_line + i);
if a.is_recognized() {
if !target_exists(&a, refs) {
out.push(format!(
"{at}: comment annotates {}{} but {} references no such {}",
a.sigil(),
a.name,
msg.id,
if a.is_term { "term" } else { "variable" },
));
}
} else if a.is_type_annotation() {
out.push(format!(
"{at}: unrecognized type annotation '({})' for {}{} — expected \
(String), (Number) or (Element)",
a.keyword,
a.sigil(),
a.name,
));
}
}
out
}
fn target_exists(a: &Annotation, refs: &[&Ref]) -> bool {
let kind = if a.is_term {
RefKind::Term
} else {
RefKind::Variable
};
refs.iter().any(|r| r.kind == kind && r.name == a.name)
}
fn detached_comments(default: &LangBundle) -> Vec<String> {
default
.standalone_comments
.iter()
.filter_map(|c| {
let a = annotation(&c.text)?;
if !a.is_type_annotation() {
return None;
}
Some(format!(
"{}:{}: type annotation '{}{} ({})' is detached from its message \
by a blank line and has no effect",
c.file,
c.line,
a.sigil(),
a.name,
a.keyword,
))
})
.collect()
}
fn ineffective_annotations(lang: &LangBundle) -> Vec<String> {
let report = |file: &str, line: usize, a: &Annotation| {
format!(
"{file}:{line}: type annotation '{}{} ({})' in non-default locale \
'{}' has no effect — type comments only apply to the default locale",
a.sigil(),
a.name,
a.keyword,
lang.language_id,
)
};
let mut out = Vec::new();
for msg in &lang.messages {
for (i, line) in msg.comment.iter().enumerate() {
if let Some(a) = annotation(line).filter(|a| a.is_type_annotation()) {
out.push(report(&msg.file, msg.comment_line + i, &a));
}
}
}
for c in &lang.standalone_comments {
if let Some(a) = annotation(&c.text).filter(|a| a.is_type_annotation()) {
out.push(report(&c.file, c.line, &a));
}
}
out
}
fn untyped_variables(msg: &Message, element_vars: &HashSet<(&str, &str)>) -> Vec<String> {
msg.variables
.iter()
.filter(|v| v.typ == VarType::Any)
.filter(|v| !element_vars.contains(&(msg.id.message.as_str(), v.id.as_str())))
.map(|v| {
format!(
"{}:{}: variable ${} in {} has no type — add a \
'# ${} (String)' or '# ${} (Number)' comment",
msg.file, msg.line, v.id, msg.id, v.id, v.id,
)
})
.collect()
}