Files
Decodal/crates/decodal-language-tools/src/lib.rs
T
2026-07-09 18:34:41 +09:00

852 lines
27 KiB
Rust

use std::{error::Error, fmt};
#[cfg(not(target_arch = "wasm32"))]
use tree_sitter::{Node, Parser, TreeCursor};
use wasm_bindgen::prelude::*;
const INDENT: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatError {
message: String,
}
impl FormatError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for FormatError {}
pub fn format_source(source: &str) -> Result<String, FormatError> {
decodal::parse_source(source)
.map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?;
format_source_impl(source)
}
#[cfg(not(target_arch = "wasm32"))]
fn format_source_impl(source: &str) -> Result<String, FormatError> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_decodal::language())
.map_err(|error| {
FormatError::new(format!("failed to load tree-sitter grammar: {error}"))
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| FormatError::new("tree-sitter failed to parse source"))?;
let root = tree.root_node();
if root.has_error() {
return Err(FormatError::new(
"parse error: source contains tree-sitter errors",
));
}
let mut formatter = Formatter::new(source);
let mut out = formatter.format_source_file(root);
if !out.ends_with('\n') {
out.push('\n');
}
Ok(out)
}
#[wasm_bindgen(js_name = formatSource)]
pub fn format_source_json(source: &str) -> String {
match format_source(source) {
Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(),
Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(),
}
}
#[cfg(target_arch = "wasm32")]
fn format_source_impl(source: &str) -> Result<String, FormatError> {
Ok(LexicalFormatter::new(source).format())
}
#[cfg(target_arch = "wasm32")]
struct LexicalFormatter<'a> {
chars: core::iter::Peekable<core::str::Chars<'a>>,
out: String,
indent: usize,
line_start: bool,
}
#[cfg(target_arch = "wasm32")]
impl<'a> LexicalFormatter<'a> {
fn new(source: &'a str) -> Self {
Self {
chars: source.chars().peekable(),
out: String::new(),
indent: 0,
line_start: true,
}
}
fn format(mut self) -> String {
while let Some(ch) = self.chars.next() {
match ch {
' ' | '\t' | '\r' | '\n' => self.consume_whitespace(ch),
'#' => self.write_comment(),
'"' => self.write_string(),
'{' => self.open_block('{'),
'[' => self.open_block('['),
'}' => self.close_block('}'),
']' => self.close_block(']'),
';' => self.end_statement(),
',' => self.write_comma(),
'.' => self.write_compact('.'),
':' => self.write_spaced_operator(":"),
'(' => self.write_compact('('),
')' => self.write_compact(')'),
'+' | '-' | '*' | '/' | '=' | '!' | '&' | '|' | '>' | '<' => {
self.write_operator(ch)
}
_ => self.write_word(ch),
}
}
self.trim_trailing_spaces();
if !self.out.ends_with('\n') {
self.out.push('\n');
}
self.out
}
fn consume_whitespace(&mut self, ch: char) {
if ch == '\n' && self.out.ends_with('\n') && !self.out.ends_with("\n\n") {
self.out.push('\n');
self.line_start = true;
}
}
fn write_comment(&mut self) {
if self.line_start {
self.write_indent();
} else {
self.ensure_space();
}
self.out.push('#');
for ch in self.chars.by_ref() {
if ch == '\n' {
break;
}
self.out.push(ch);
}
self.trim_trailing_spaces();
self.out.push('\n');
self.line_start = true;
}
fn write_string(&mut self) {
self.write_indent_if_needed();
self.ensure_word_boundary();
self.out.push('"');
let mut escaped = false;
for ch in self.chars.by_ref() {
self.out.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
break;
}
}
}
fn open_block(&mut self, ch: char) {
self.trim_trailing_spaces();
self.out.push(' ');
self.out.push(ch);
self.out.push('\n');
self.indent += INDENT;
self.line_start = true;
}
fn close_block(&mut self, ch: char) {
self.trim_trailing_spaces();
if !self.out.ends_with('\n') {
self.out.push('\n');
}
self.indent = self.indent.saturating_sub(INDENT);
self.write_indent();
self.out.push(ch);
self.line_start = false;
}
fn end_statement(&mut self) {
self.trim_trailing_spaces();
self.out.push(';');
self.out.push('\n');
self.line_start = true;
}
fn write_comma(&mut self) {
self.trim_trailing_spaces();
self.out.push(',');
self.out.push(' ');
self.line_start = false;
}
fn write_operator(&mut self, ch: char) {
let mut op = String::new();
op.push(ch);
if let Some(next) = self.chars.peek().copied() {
let two_char = matches!(
(ch, next),
('=', '=')
| ('!', '=')
| ('>', '=')
| ('<', '=')
| ('+', '+')
| ('/', '/')
| ('&', '&')
| ('|', '|')
| ('=', '>')
);
if two_char {
op.push(next);
self.chars.next();
}
}
self.write_spaced_operator(&op);
}
fn write_spaced_operator(&mut self, op: &str) {
self.trim_trailing_spaces();
self.ensure_space();
self.out.push_str(op);
self.out.push(' ');
self.line_start = false;
}
fn write_compact(&mut self, ch: char) {
self.trim_trailing_spaces();
self.write_indent_if_needed();
self.out.push(ch);
self.line_start = false;
}
fn write_word(&mut self, first: char) {
self.write_indent_if_needed();
self.ensure_word_boundary();
self.out.push(first);
while let Some(next) = self.chars.peek().copied() {
if next.is_alphanumeric() || next == '_' {
self.out.push(next);
self.chars.next();
} else {
break;
}
}
self.line_start = false;
}
fn write_indent_if_needed(&mut self) {
if self.line_start {
self.write_indent();
self.line_start = false;
}
}
fn write_indent(&mut self) {
for _ in 0..self.indent {
self.out.push(' ');
}
}
fn ensure_word_boundary(&mut self) {
let needs_space = self
.out
.chars()
.last()
.is_some_and(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '"'));
if needs_space {
self.out.push(' ');
}
}
fn ensure_space(&mut self) {
let needs_space = self
.out
.chars()
.last()
.is_some_and(|ch| !matches!(ch, ' ' | '\n' | '.' | '(' | '['));
if needs_space {
self.out.push(' ');
}
}
fn trim_trailing_spaces(&mut self) {
while self.out.ends_with(' ') || self.out.ends_with('\t') {
self.out.pop();
}
}
}
#[cfg(not(target_arch = "wasm32"))]
struct Formatter<'a> {
source: &'a str,
}
#[cfg(not(target_arch = "wasm32"))]
impl<'a> Formatter<'a> {
fn new(source: &'a str) -> Self {
Self { source }
}
fn format_source_file(&mut self, node: Node<'a>) -> String {
let mut out = String::new();
let children = named_children(node);
self.write_statement_list(&mut out, &children, 0);
out
}
fn write_statement_list(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
let mut pending_comments = Vec::new();
let mut pending_blank = false;
let mut previous_end = 0;
let mut previous_statement_end_row = None;
for &child in children {
let is_trailing_comment = child.kind() == "comment"
&& previous_statement_end_row == Some(child.start_position().row);
if previous_statement_end_row.is_some() && !is_trailing_comment {
if !out.ends_with('\n') {
out.push('\n');
}
previous_statement_end_row = None;
}
if previous_end > 0 && has_blank_line(self.slice(previous_end, child.start_byte())) {
pending_blank = true;
}
if child.kind() == "comment" {
if is_trailing_comment {
if !out.ends_with(' ') {
out.push(' ');
}
out.push_str(self.raw_trimmed(child));
out.push('\n');
previous_statement_end_row = None;
} else {
pending_comments.push(child);
}
previous_end = child.end_byte();
continue;
}
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
pending_blank = false;
for comment in pending_comments.drain(..) {
write_indent(out, indent);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
write_indent(out, indent);
self.write_statement(out, child, indent);
previous_statement_end_row = Some(child.end_position().row);
previous_end = child.end_byte();
}
if previous_statement_end_row.is_some() && !out.ends_with('\n') {
out.push('\n');
}
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
for comment in pending_comments {
write_indent(out, indent);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
}
fn write_statement(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
match node.kind() {
"field_definition" => {
self.write_field_definition(out, node, indent);
out.push(';');
}
_ => {
self.write_expr(out, node, indent, 0);
out.push(';');
}
}
}
fn write_field_definition(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
if let Some(path) = node.child_by_field_name("path") {
self.write_field_path(out, path);
} else {
out.push_str(self.raw_trimmed(node));
return;
}
out.push_str(" = ");
if let Some(value) = node.child_by_field_name("value") {
self.write_expr(out, value, indent, 0);
}
}
fn write_field_path(&mut self, out: &mut String, node: Node<'a>) {
let identifiers: Vec<_> = named_children(node)
.into_iter()
.filter(|child| child.kind() == "identifier")
.collect();
if identifiers.is_empty() {
out.push_str(self.raw_trimmed(node));
return;
}
for (index, identifier) in identifiers.into_iter().enumerate() {
if index > 0 {
out.push('.');
}
out.push_str(self.raw_trimmed(identifier));
}
}
fn write_expr(&mut self, out: &mut String, node: Node<'a>, indent: usize, parent_prec: u8) {
if expression_contains_comment(node)
&& !matches!(
node.kind(),
"object" | "array" | "let_expression" | "match_expression"
)
{
out.push_str(self.raw_trimmed(node));
return;
}
let prec = self.precedence(node);
let parenthesize = prec < parent_prec;
if parenthesize {
out.push('(');
}
match node.kind() {
"literal" => {
if let Some(child) = named_children(node).first().copied() {
out.push_str(self.raw_trimmed(child));
} else {
out.push_str(self.raw_trimmed(node));
}
}
"identifier" | "string" | "integer" | "float" | "boolean" | "regex_literal" => {
out.push_str(self.raw_trimmed(node));
}
"comparison_constraint" => self.write_comparison_constraint(out, node, indent),
"object" => self.write_object(out, node, indent),
"array" => self.write_array(out, node, indent),
"let_expression" => self.write_let(out, node, indent),
"function_expression" => self.write_function(out, node, indent),
"match_expression" => self.write_match(out, node, indent),
"import_expression" => self.write_import(out, node),
"parenthesized_expression" => {
out.push('(');
if let Some(expr) = named_children(node)
.into_iter()
.find(|child| child.kind() != "comment")
{
self.write_expr(out, expr, indent, 0);
}
out.push(')');
}
"call_expression" => self.write_call(out, node, indent),
"path_expression" => self.write_path(out, node, indent),
"unary_expression" => self.write_unary(out, node, indent),
"binary_expression" | "default_expression" => {
self.write_binary_like(out, node, indent, prec)
}
_ => out.push_str(self.raw_trimmed(node)),
}
if parenthesize {
out.push(')');
}
}
fn write_object(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
let children = named_children(node);
if children.is_empty() {
out.push_str("{}");
return;
}
out.push('{');
out.push('\n');
self.write_statement_list(out, &children, indent + INDENT);
write_indent(out, indent);
out.push('}');
}
fn write_array(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
let children = named_children(node);
if children.is_empty() {
out.push_str("[]");
return;
}
let has_comment = children.iter().any(|child| child.kind() == "comment");
let inline = !has_comment && children.iter().all(|child| is_inline_expr(*child));
if inline {
out.push('[');
for (index, child) in children.into_iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.write_expr(out, child, indent, 0);
}
out.push(']');
return;
}
out.push('[');
out.push('\n');
let mut pending_comments = Vec::new();
for child in children {
if child.kind() == "comment" {
pending_comments.push(child);
continue;
}
for comment in pending_comments.drain(..) {
write_indent(out, indent + INDENT);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
write_indent(out, indent + INDENT);
self.write_expr(out, child, indent + INDENT, 0);
out.push_str(",\n");
}
for comment in pending_comments {
write_indent(out, indent + INDENT);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
write_indent(out, indent);
out.push(']');
}
fn write_let(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
let body = node.child_by_field_name("body");
out.push_str("let");
out.push('\n');
let children: Vec<_> = named_children(node)
.into_iter()
.filter(|child| Some(*child) != body)
.collect();
self.write_statement_list(out, &children, indent + INDENT);
write_indent(out, indent);
out.push_str("in");
if let Some(body) = body {
out.push('\n');
write_indent(out, indent + INDENT);
self.write_expr(out, body, indent + INDENT, 0);
}
}
fn write_function(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
let body = node.child_by_field_name("body");
let params: Vec<_> = named_children(node)
.into_iter()
.filter(|child| child.kind() == "parameter")
.collect();
out.push('(');
for (index, param) in params.into_iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.write_parameter(out, param, indent);
}
out.push_str(") =>");
if let Some(body) = body {
if is_inline_expr(body) {
out.push(' ');
self.write_expr(out, body, indent, 0);
} else {
out.push('\n');
write_indent(out, indent + INDENT);
self.write_expr(out, body, indent + INDENT, 0);
}
}
}
fn write_parameter(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
if let Some(name) = node.child_by_field_name("name") {
out.push_str(self.raw_trimmed(name));
}
if let Some(constraint) = node.child_by_field_name("constraint") {
out.push_str(": ");
self.write_expr(out, constraint, indent, 0);
}
}
fn write_match(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
out.push_str("match ");
if let Some(scrutinee) = node.child_by_field_name("scrutinee") {
self.write_expr(out, scrutinee, indent, 0);
}
out.push_str(" {");
let children: Vec<_> = named_children(node)
.into_iter()
.filter(|child| child.kind() == "match_arm" || child.kind() == "comment")
.collect();
if !children.is_empty() {
out.push('\n');
self.write_match_arms(out, &children, indent + INDENT);
write_indent(out, indent);
}
out.push('}');
}
fn write_match_arms(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
let mut pending_comments = Vec::new();
for &child in children {
if child.kind() == "comment" {
pending_comments.push(child);
continue;
}
for comment in pending_comments.drain(..) {
write_indent(out, indent);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
write_indent(out, indent);
if let Some(pattern) = child.child_by_field_name("pattern") {
self.write_expr(out, pattern, indent, 0);
}
out.push_str(": ");
if let Some(body) = child.child_by_field_name("body") {
self.write_expr(out, body, indent, 0);
}
out.push_str(";\n");
}
for comment in pending_comments {
write_indent(out, indent);
out.push_str(self.raw_trimmed(comment));
out.push('\n');
}
}
fn write_import(&mut self, out: &mut String, node: Node<'a>) {
out.push_str("import ");
if let Some(specifier) = node.child_by_field_name("specifier") {
out.push_str(self.raw_trimmed(specifier));
}
}
fn write_call(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
let callee = node.child_by_field_name("function");
if let Some(callee) = callee {
self.write_expr(out, callee, indent, self.precedence(node));
}
out.push('(');
let args: Vec<_> = named_children(node)
.into_iter()
.filter(|child| Some(*child) != callee && child.kind() != "comment")
.collect();
for (index, arg) in args.into_iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.write_expr(out, arg, indent, 0);
}
out.push(')');
}
fn write_path(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
if let Some(object) = node.child_by_field_name("object") {
self.write_expr(out, object, indent, self.precedence(node));
}
out.push('.');
if let Some(field) = node.child_by_field_name("field") {
out.push_str(self.raw_trimmed(field));
}
}
fn write_unary(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
if let Some(operand) = node.child_by_field_name("operand") {
let operator = self.slice(node.start_byte(), operand.start_byte()).trim();
out.push_str(operator);
self.write_expr(out, operand, indent, self.precedence(node));
} else {
out.push_str(self.raw_trimmed(node));
}
}
fn write_comparison_constraint(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
if let Some(value) = node.child_by_field_name("value") {
let operator = self.slice(node.start_byte(), value.start_byte()).trim();
out.push_str(operator);
out.push(' ');
self.write_expr(out, value, indent, self.precedence(node));
} else {
out.push_str(self.raw_trimmed(node));
}
}
fn write_binary_like(&mut self, out: &mut String, node: Node<'a>, indent: usize, prec: u8) {
let left = node
.child_by_field_name("left")
.or_else(|| node.child_by_field_name("base"));
let right = node
.child_by_field_name("right")
.or_else(|| node.child_by_field_name("fallback"));
let (Some(left), Some(right)) = (left, right) else {
out.push_str(self.raw_trimmed(node));
return;
};
self.write_expr(out, left, indent, prec);
out.push(' ');
if node.kind() == "default_expression" {
out.push_str("default");
} else {
out.push_str(self.slice(left.end_byte(), right.start_byte()).trim());
}
out.push(' ');
self.write_expr(out, right, indent, prec + 1);
}
fn precedence(&self, node: Node<'a>) -> u8 {
match node.kind() {
"default_expression" => 1,
"binary_expression" => self.binary_precedence(node),
"unary_expression" | "comparison_constraint" => 17,
"call_expression" | "path_expression" => 19,
_ => 20,
}
}
fn binary_precedence(&self, node: Node<'a>) -> u8 {
let Some(left) = node.child_by_field_name("left") else {
return 10;
};
let Some(right) = node.child_by_field_name("right") else {
return 10;
};
match self.slice(left.end_byte(), right.start_byte()).trim() {
"//" => 3,
"&" => 5,
"||" => 7,
"&&" => 9,
"==" | "!=" | ">" | ">=" | "<" | "<=" => 11,
"++" => 12,
"+" | "-" => 13,
"*" | "/" => 15,
_ => 10,
}
}
fn raw_trimmed(&self, node: Node<'a>) -> &'a str {
self.slice(node.start_byte(), node.end_byte()).trim()
}
fn slice(&self, start: usize, end: usize) -> &'a str {
&self.source[start..end]
}
}
#[cfg(not(target_arch = "wasm32"))]
fn named_children<'a>(node: Node<'a>) -> Vec<Node<'a>> {
let mut cursor: TreeCursor<'a> = node.walk();
node.named_children(&mut cursor).collect()
}
#[cfg(not(target_arch = "wasm32"))]
fn expression_contains_comment(node: Node<'_>) -> bool {
if node.kind() == "comment" {
return true;
}
named_children(node)
.into_iter()
.any(expression_contains_comment)
}
#[cfg(not(target_arch = "wasm32"))]
fn is_inline_expr(node: Node<'_>) -> bool {
match node.kind() {
"literal"
| "identifier"
| "string"
| "integer"
| "float"
| "boolean"
| "regex_literal"
| "comparison_constraint"
| "import_expression" => true,
"path_expression"
| "call_expression"
| "unary_expression"
| "binary_expression"
| "default_expression"
| "parenthesized_expression" => {
!expression_contains_comment(node)
&& named_children(node).into_iter().all(is_inline_expr)
}
"array" => {
!expression_contains_comment(node)
&& named_children(node).into_iter().all(is_inline_expr)
}
_ => false,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn has_blank_line(text: &str) -> bool {
text.bytes().filter(|byte| *byte == b'\n').count() >= 2
}
#[cfg(not(target_arch = "wasm32"))]
fn write_indent(out: &mut String, indent: usize) {
for _ in 0..indent {
out.push(' ');
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_fields_and_preserves_comments() {
let source =
"Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n"
);
}
#[test]
fn formats_arrays_and_match_comments() {
let source = "result=match x{# c\n_: [1,{a=2;}];};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n"
);
}
#[test]
fn wasm_export_returns_json() {
let output = format_source_json("value={a=1;};");
assert!(output.contains("\"ok\":true"));
assert!(output.contains("value = {\\n a = 1;\\n};\\n"));
}
}