use super::diagnostic::{Diagnostic, DiagnosticCode, Location, Severity};
use std::collections::HashSet;
pub fn detect_unknown_fields(toml_str: &str) -> Vec<Diagnostic> {
let doc: toml_edit::DocumentMut = match toml_str.parse() {
Ok(d) => d,
Err(_) => return vec![],
};
let known_top_level = known_top_level_tables();
let mut diagnostics = Vec::new();
for (key, item) in doc.iter() {
if !known_top_level.contains(key) {
let (line, column) = position_for_key(&doc, key);
let code = if item.is_table() || item.is_inline_table() || item.is_array_of_tables() {
DiagnosticCode::UnknownTable
} else {
DiagnosticCode::UnknownField
};
let kind = if item.is_table() || item.is_array_of_tables() {
"table"
} else {
"field"
};
diagnostics.push(Diagnostic {
severity: Severity::Error,
code,
message: format!("unknown {} `{}`", kind, key),
location: Location {
line,
column,
field: key.to_string(),
},
});
}
}
diagnostics
}
fn known_top_level_tables() -> HashSet<&'static str> {
[
"metadata",
"tone",
"white_balance",
"lut",
"hsl",
"vignette",
"color_grading",
"tone_curve",
"detail",
"dehaze",
"noise_reduction",
"grain",
]
.into_iter()
.collect()
}
pub(super) fn find_position_by_path(source: &str, path: &str) -> (usize, usize) {
let parts: Vec<&str> = path.split('.').collect();
match parts.as_slice() {
[single] => find_key_position(source, single, None),
[parent, child] => find_key_position(source, child, Some(parent)),
_ => {
let full_parent = parts[..parts.len() - 1].join(".");
let child = parts[parts.len() - 1];
find_key_position(source, child, Some(&full_parent))
}
}
}
fn position_for_key(doc: &toml_edit::DocumentMut, key: &str) -> (usize, usize) {
let source = doc.to_string();
find_key_position(&source, key, None)
}
fn find_key_position(source: &str, key: &str, parent: Option<&str>) -> (usize, usize) {
let mut in_parent = parent.is_none();
let parent_heading = parent.map(|p| format!("[{}]", p));
for (idx, line) in source.lines().enumerate() {
let line_num = idx + 1;
let trimmed = line.trim_start();
if let Some(p) = parent {
let dotted_heading = format!("[{}.{}]", p, key);
let dotted_heading_with_dot = format!("[{}.{}.", p, key);
let dotted_array = format!("[[{}.{}]]", p, key);
if trimmed.starts_with(dotted_heading.as_str())
|| trimmed.starts_with(dotted_heading_with_dot.as_str())
|| trimmed.starts_with(dotted_array.as_str())
{
let column = line.len() - line.trim_start().len() + 1;
return (line_num, column);
}
}
if let Some(ref heading) = parent_heading {
if trimmed.starts_with(heading.as_str()) {
in_parent = true;
continue;
}
if trimmed.starts_with('[') && in_parent {
in_parent = false;
}
}
if !in_parent {
continue;
}
let heading_match = trimmed.starts_with(&format!("[{}]", key))
|| trimmed.starts_with(&format!("[{}.", key))
|| trimmed.starts_with(&format!("[[{}]]", key));
let field_match =
trimmed.starts_with(&format!("{} =", key)) || trimmed.starts_with(&format!("{}=", key));
if heading_match || field_match {
let column = line.len() - line.trim_start().len() + 1;
return (line_num, column);
}
}
(1, 1)
}