use std::io;
use pretty_assertions::StrComparison;
use tree_sitter::Position;
pub use crate::{
error::{FormatterError, IoError},
language::Language,
tree_sitter::{
CoverageData, SyntaxNode, TopiaryQuery, Visualisation, apply_query, check_query_coverage,
parse,
},
};
mod atom_collection;
mod error;
mod graphviz;
mod language;
mod pretty;
mod tree_sitter;
#[doc(hidden)]
pub mod test_utils;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScopeInformation {
line_number: u32,
scope_id: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Capitalisation {
UpperCase,
LowerCase,
#[default]
Pass,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Atom {
Blankline,
#[default]
Empty,
Hardline,
IndentEnd,
IndentStart,
Leaf {
content: String,
id: usize,
original_position: Position,
single_line_no_indent: bool,
multi_line_indent_all: bool,
keep_whitespace: bool,
capitalisation: Capitalisation,
},
Literal(String),
Softline {
spaced: bool,
},
Space,
Antispace,
DeleteBegin,
DeleteEnd,
CaseBegin(Capitalisation),
CaseEnd,
ScopeBegin(ScopeInformation),
ScopeEnd(ScopeInformation),
MeasuringScopeBegin(ScopeInformation),
MeasuringScopeEnd(ScopeInformation),
ScopedSoftline {
id: usize,
scope_id: String,
spaced: bool,
},
ScopedConditional {
id: usize,
scope_id: String,
condition: ScopeCondition,
atom: Box<Atom>,
},
}
impl Atom {
pub(crate) fn dominates(&self, other: &Atom) -> bool {
match self {
Atom::Empty => false,
Atom::Space => matches!(other, Atom::Empty),
Atom::Hardline => matches!(other, Atom::Space | Atom::Empty),
Atom::Blankline => matches!(other, Atom::Hardline | Atom::Space | Atom::Empty),
_ => panic!("Unexpected character in is_dominant"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScopeCondition {
SingleLineOnly,
MultiLineOnly,
}
pub type FormatterResult<T> = std::result::Result<T, FormatterError>;
#[derive(Clone, Copy, Debug)]
pub enum Operation {
Format {
skip_idempotence: bool,
tolerate_parsing_errors: bool,
},
Visualise {
output_format: Visualisation,
},
}
pub fn formatter(
input: &mut impl io::Read,
output: &mut impl io::Write,
language: &Language,
operation: Operation,
) -> FormatterResult<()> {
let content = read_input(input).map_err(|e| {
FormatterError::Io(IoError::Filesystem(
"Failed to read input contents".into(),
e,
))
})?;
formatter_str(&content, output, language, operation)
}
pub fn formatter_str(
input: &str,
output: &mut impl io::Write,
language: &Language,
operation: Operation,
) -> FormatterResult<()> {
let tolerate_parsing_errors = match operation {
Operation::Format {
tolerate_parsing_errors,
..
} => tolerate_parsing_errors,
_ => false,
};
let tree = tree_sitter::parse(input, &language.grammar, tolerate_parsing_errors)?;
formatter_tree(tree, input, output, language, operation)?;
Ok(())
}
pub fn formatter_tree(
tree: topiary_tree_sitter_facade::Tree,
input_content: &str,
output: &mut impl io::Write,
language: &Language,
operation: Operation,
) -> FormatterResult<()> {
match operation {
Operation::Format {
skip_idempotence,
tolerate_parsing_errors,
} => {
log::debug!("Apply Tree-sitter query");
let mut atoms = tree_sitter::apply_query_tree(tree, input_content, &language.query)?;
atoms.post_process();
log::debug!("Pretty-print output");
let rendered = pretty::render(
&atoms[..],
language.indent.as_ref().map_or(" ", |v| v.as_str()),
)?;
let rendered = format!("{}\n", rendered.trim());
if !skip_idempotence {
idempotence_check(&rendered, language, tolerate_parsing_errors)?;
}
write!(output, "{rendered}")?;
}
Operation::Visualise { output_format } => {
let root: SyntaxNode = tree.root_node().into();
match output_format {
Visualisation::GraphViz => graphviz::write(output, &root)?,
Visualisation::Json => serde_json::to_writer(output, &root)?,
};
}
};
Ok(())
}
fn read_input(input: &mut dyn io::Read) -> Result<String, io::Error> {
let mut content = String::new();
input.read_to_string(&mut content)?;
Ok(content)
}
fn idempotence_check(
content: &str,
language: &Language,
tolerate_parsing_errors: bool,
) -> FormatterResult<()> {
log::info!("Checking for idempotence ...");
let mut input = content.as_bytes();
let mut output = io::BufWriter::new(Vec::new());
match formatter(
&mut input,
&mut output,
language,
Operation::Format {
skip_idempotence: true,
tolerate_parsing_errors,
},
) {
Ok(()) => {
let reformatted = String::from_utf8(output.into_inner()?)?;
if content == reformatted {
Ok(())
} else {
log::error!("Failed idempotence check");
log::error!("{}", StrComparison::new(content, &reformatted));
Err(FormatterError::Idempotence)
}
}
Err(error @ FormatterError::Parsing { .. }) => {
Err(FormatterError::IdempotenceParsing(Box::new(error)))
}
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use std::fs;
use test_log::test;
use crate::{
Language, Operation, TopiaryQuery, error::FormatterError, formatter,
test_utils::pretty_assert_eq,
};
#[test(tokio::test)]
async fn parsing_error_fails_formatting() {
let mut input = r#"{"foo":{"bar"}}"#.as_bytes();
let mut output = Vec::new();
let query_content = "(#language! json)";
let grammar = topiary_tree_sitter_facade::Language::from(tree_sitter_json::LANGUAGE);
let language = Language {
name: "json".to_owned(),
query: TopiaryQuery::new(&grammar, query_content).unwrap(),
grammar,
indent: None,
};
match formatter(
&mut input,
&mut output,
&language,
Operation::Format {
skip_idempotence: true,
tolerate_parsing_errors: false,
},
) {
Err(FormatterError::Parsing(node))
if node.start_point().row() == 0 && node.end_point().row() == 0 => {}
result => {
panic!("Expected a parsing error on line 1, but got {result:?}");
}
}
}
#[test(tokio::test)]
async fn tolerate_parsing_errors() {
let mut input = "{\"one\":{\"bar\" \"baz\"},\"two\":\"bar\"}".as_bytes();
let expected = "{ \"one\": {\"bar\" \"baz\"}, \"two\": \"bar\" }\n";
let mut output = Vec::new();
let query_content = fs::read_to_string("../topiary-queries/queries/json.scm").unwrap();
let grammar = tree_sitter_json::LANGUAGE.into();
let language = Language {
name: "json".to_owned(),
query: TopiaryQuery::new(&grammar, &query_content).unwrap(),
grammar,
indent: None,
};
formatter(
&mut input,
&mut output,
&language,
Operation::Format {
skip_idempotence: true,
tolerate_parsing_errors: true,
},
)
.unwrap();
let formatted = String::from_utf8(output).unwrap();
log::debug!("{formatted}");
pretty_assert_eq(expected, &formatted);
}
}