difftreelog
refactor use new rowan-parser in jrsonnet-fmt
in: master
6 files changed
Cargo.lockdiffbeforeafterbothno changes
cmds/jrsonnet-fmt/Cargo.tomldiffbeforeafterboth--- a/cmds/jrsonnet-fmt/Cargo.toml
+++ b/cmds/jrsonnet-fmt/Cargo.toml
@@ -4,8 +4,11 @@
edition = "2021"
[dependencies]
-dprint-core = "0.60.0"
+dprint-core = "0.63.2"
jrsonnet-rowan-parser.workspace = true
insta = "1.15"
indoc = "1.0"
ass-stroke = { path = "/home/lach/build/ass-stroke/crates/ass-stroke", version = "0.1.0" }
+clap = { version = "4.4.2", features = ["derive"] }
+tempfile = "3.8.0"
+thiserror = "1.0.48"
cmds/jrsonnet-fmt/src/children.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/children.rs
+++ b/cmds/jrsonnet-fmt/src/children.rs
@@ -99,15 +99,19 @@
node: SyntaxNode,
start: Option<&SyntaxElement>,
end: Option<&SyntaxElement>,
+ trailing: Option<ChildTrivia>,
) -> (Vec<Child<T>>, EndingComments) {
let mut iter = node.children_with_tokens().peekable();
- while iter.peek() != start {
+ if start.is_some() {
+ while iter.peek() != start {
+ iter.next();
+ }
iter.next();
}
- iter.next();
children(
iter.take_while(|i| Some(i) != end),
- start.is_none() || end.is_none(),
+ start.is_none() && end.is_none(),
+ trailing,
)
}
@@ -165,6 +169,7 @@
pub fn children<T: AstNode + Debug>(
items: impl Iterator<Item = SyntaxElement>,
loose: bool,
+ mut trailing: Option<ChildTrivia>,
) -> (Vec<Child<T>>, EndingComments) {
let mut out = Vec::new();
let mut current_child = None::<Child<T>>;
@@ -175,7 +180,12 @@
for item in items {
if let Some(value) = item.as_node().cloned().and_then(T::cast) {
- let before_trivia = mem::take(&mut next);
+ let before_trivia = if let Some(trailing) = trailing.take() {
+ assert!(next.is_empty());
+ trailing
+ } else {
+ mem::take(&mut next)
+ };
let last_child = current_child.replace(Child {
// First item should not start with newline
should_start_with_newline: had_some
@@ -195,7 +205,10 @@
} else if let Some(trivia) = item.as_token().cloned().and_then(Trivia::cast) {
let is_single_line_comment = trivia.kind() == TriviaKind::SingleLineHashComment
|| trivia.kind() == TriviaKind::SingleLineSlashComment;
- if started_next
+ if trailing.is_some() {
+ // Someone have already parsed trivia for us
+ continue;
+ } else if started_next
|| current_child.is_none()
|| trivia.text().contains('\n') && !is_single_line_comment
{
@@ -271,4 +284,7 @@
pub fn is_empty(&self) -> bool {
!self.should_start_with_newline && self.trivia.is_empty()
}
+ pub fn extract_trailing(&mut self) -> ChildTrivia {
+ mem::take(&mut self.trivia)
+ }
}
cmds/jrsonnet-fmt/src/comments.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/comments.rs
+++ b/cmds/jrsonnet-fmt/src/comments.rs
@@ -24,11 +24,11 @@
let sliced = &text[..pos];
p!(pi: string(sliced.to_string()));
text = &text[pos..];
- if! text.is_empty(){
+ if !text.is_empty() {
match text.as_bytes()[0] {
b'\n' => p!(pi: nl),
b'\t' => p!(pi: tab),
- _ => unreachable!()
+ _ => unreachable!(),
}
text = &text[1..];
}
@@ -69,7 +69,10 @@
lines.pop();
}
if lines.len() == 1 && !doc {
- p!(pi: str("/* ") string(lines[0].trim().to_string()) str(" */") nl)
+ if matches!(loc, CommentLocation::ItemInline) {
+ p!(pi: str(" "));
+ }
+ p!(pi: str("/* ") string(lines[0].trim().to_string()) str(" */"))
} else if !lines.is_empty() {
fn common_ws_prefix<'a>(a: &'a str, b: &str) -> &'a str {
let offset = a
cmds/jrsonnet-fmt/src/main.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/main.rs
+++ b/cmds/jrsonnet-fmt/src/main.rs
@@ -1,15 +1,21 @@
-use std::any::type_name;
+use std::{
+ any::type_name,
+ fs,
+ io::{self, Write},
+ path::PathBuf,
+ process,
+};
use children::{children_between, trivia_before};
+use clap::Parser;
use dprint_core::formatting::{PrintItems, PrintOptions};
use jrsonnet_rowan_parser::{
nodes::{
ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
- DestructRest, Expr, FieldName, ForSpec, IfSpec, ImportKind, LhsExpr, Literal, Member, Name,
- Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Text, UnaryOperator,
- Visibility, VisibilityKind,
+ DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
+ Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
+ UnaryOperator, Visibility,
},
- rowan::NodeOrToken,
AstNode, AstToken, SyntaxToken,
};
@@ -287,7 +293,9 @@
Member::MemberFieldNormal(n) => {
p!(new: {n.field_name()} if(n.plus_token().is_some())({n.plus_token()}) {n.visibility()} str(" ") {n.expr()})
}
- Member::MemberFieldMethod(_) => todo!(),
+ Member::MemberFieldMethod(m) => {
+ p!(new: {m.field_name()} {m.params_desc()} {m.visibility()} str(" ") {m.expr()})
+ }
}
}
}
@@ -296,7 +304,7 @@
fn print(&self) -> PrintItems {
match self {
ObjBody::ObjBodyComp(l) => {
- let (children, end_comments) = children_between::<Member>(
+ let (children, mut end_comments) = children_between::<Member>(
l.syntax().clone(),
l.l_brace_token().map(Into::into).as_ref(),
Some(
@@ -307,7 +315,9 @@
.clone())
.into(),
),
+ None,
);
+ let trailing_for_comp = end_comments.extract_trailing();
let mut pi = p!(new: str("{") >i nl);
for mem in children.into_iter() {
if mem.should_start_with_newline {
@@ -333,6 +343,7 @@
.or_else(|| l.l_brace_token().map(Into::into))
.as_ref(),
l.r_brace_token().map(Into::into).as_ref(),
+ Some(trailing_for_comp),
);
for mem in compspecs.into_iter() {
if mem.should_start_with_newline {
@@ -347,7 +358,7 @@
}
p!(pi: items(format_comments(&end_comments.trivia, CommentLocation::EndOfItems)));
- p!(pi: <i str("}"));
+ p!(pi: nl <i str("}"));
pi
}
ObjBody::ObjBodyMemberList(l) => {
@@ -355,6 +366,7 @@
l.syntax().clone(),
l.l_brace_token().map(Into::into).as_ref(),
l.r_brace_token().map(Into::into).as_ref(),
+ None,
);
if children.is_empty() && end_comments.is_empty() {
return p!(new: str("{ }"));
@@ -412,11 +424,6 @@
p!(new: string(self.syntax().to_string()))
}
}
-impl Printable for LhsExpr {
- fn print(&self) -> PrintItems {
- p!(new: {self.expr()})
- }
-}
impl Printable for ForSpec {
fn print(&self) -> PrintItems {
p!(new: str("for ") {self.bind()} str(" in ") {self.expr()})
@@ -437,62 +444,75 @@
}
impl Printable for Expr {
fn print(&self) -> PrintItems {
+ let mut o = p!(new:);
+ let (stmts, ending) = children_between::<Stmt>(
+ self.syntax().clone(),
+ None,
+ self.expr_base()
+ .as_ref()
+ .map(ExprBase::syntax)
+ .cloned()
+ .map(Into::into)
+ .as_ref(),
+ None,
+ );
+ for stmt in stmts {
+ p!(o: {stmt.value});
+ }
+ p!(o: {self.expr_base()});
+ let (suffixes, ending) = children_between::<Suffix>(
+ self.syntax().clone(),
+ self.expr_base()
+ .as_ref()
+ .map(ExprBase::syntax)
+ .cloned()
+ .map(Into::into)
+ .as_ref(),
+ None,
+ None,
+ );
+ for suffix in suffixes {
+ p!(o: {suffix.value});
+ }
+ o
+ }
+}
+impl Printable for Suffix {
+ fn print(&self) -> PrintItems {
+ let mut o = p!(new:);
match self {
- Expr::ExprBinary(b) => {
- p!(new: {b.lhs()} str(" ") {b.binary_operator()} str(" ") {b.rhs()})
- }
- Expr::ExprUnary(u) => p!(new: {u.unary_operator()} {u.rhs()}),
- Expr::ExprSlice(s) => {
- p!(new: {s.expr()} {s.slice_desc()})
- }
- Expr::ExprIndex(i) => {
- p!(new: {i.expr()} str(".") {i.index()})
- }
- Expr::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),
- Expr::ExprApply(a) => {
- let mut pi = p!(new: {a.expr()} {a.args_desc()});
- if a.tailstrict_kw_token().is_some() {
- p!(pi: str(" tailstrict"));
+ Suffix::SuffixIndex(i) => {
+ if i.question_mark_token().is_some() {
+ p!(o: str("?"));
}
- pi
- }
- Expr::ExprObjExtend(ex) => {
- p!(new: {ex.lhs_expr()} str(" ") {ex.expr()})
- }
- Expr::ExprParened(p) => {
- p!(new: str("(") {p.expr()} str(")"))
+ p!(o: str(".") {i.index()});
}
- Expr::ExprString(s) => p!(new: {s.text()}),
- Expr::ExprNumber(n) => p!(new: {n.number()}),
- Expr::ExprArray(a) => {
- let mut pi = p!(new: str("[") >i nl);
- for el in a.exprs() {
- p!(pi: {el} str(",") nl);
+ Suffix::SuffixIndexExpr(e) => {
+ if e.question_mark_token().is_some() {
+ p!(o: str(".?"));
}
- p!(pi: <i str("]"));
- pi
+ p!(o: str("[") {e.index()} str("]"))
}
- Expr::ExprObject(o) => {
- p!(new: {o.obj_body()})
+ Suffix::SuffixSlice(d) => {
+ p!(o: {d.slice_desc()})
}
- Expr::ExprArrayComp(arr) => {
- let mut pi = p!(new: str("[") {arr.expr()});
- for spec in arr.comp_specs() {
- p!(pi: str(" ") {spec});
- }
- p!(pi: str("]"));
- pi
+ Suffix::SuffixApply(a) => {
+ p!(o: {a.args_desc()})
}
- Expr::ExprImport(v) => {
- p!(new: {v.import_kind()} str(" ") {v.text()})
- }
- Expr::ExprVar(n) => p!(new: {n.name()}),
- Expr::ExprLocal(l) => {
+ }
+ o
+ }
+}
+impl Printable for Stmt {
+ fn print(&self) -> PrintItems {
+ match self {
+ Stmt::StmtLocal(l) => {
let mut pi = p!(new:);
let (binds, end_comments) = children_between::<Bind>(
l.syntax().clone(),
l.local_kw_token().map(Into::into).as_ref(),
l.semi_token().map(Into::into).as_ref(),
+ None,
);
if binds.len() == 1 {
let bind = &binds[0];
@@ -516,24 +536,69 @@
p!(pi: <i);
}
p!(pi: str(";") nl);
-
- let expr_comments = trivia_between(
- l.syntax().clone(),
- l.semi_token().map(Into::into).as_ref(),
- l.expr()
- .map(|e| e.syntax().clone())
- .map(Into::into)
- .as_ref(),
- );
-
- if expr_comments.should_start_with_newline {
- p!(pi: nl);
+ pi
+ }
+ Stmt::StmtAssert(a) => {
+ p!(new: {a.assertion()} str(";") nl)
+ }
+ }
+ }
+}
+impl Printable for ExprBase {
+ fn print(&self) -> PrintItems {
+ match self {
+ Self::ExprBinary(b) => {
+ p!(new: {b.lhs_work()} str(" ") {b.binary_operator()} str(" ") {b.rhs_work()})
+ }
+ Self::ExprUnary(u) => p!(new: {u.unary_operator()} {u.rhs()}),
+ // Self::ExprSlice(s) => {
+ // p!(new: {s.expr()} {s.slice_desc()})
+ // }
+ // Self::ExprIndex(i) => {
+ // p!(new: {i.expr()} str(".") {i.index()})
+ // }
+ // Self::ExprIndexExpr(i) => p!(new: {i.base()} str("[") {i.index()} str("]")),
+ // Self::ExprApply(a) => {
+ // let mut pi = p!(new: {a.expr()} {a.args_desc()});
+ // if a.tailstrict_kw_token().is_some() {
+ // p!(pi: str(" tailstrict"));
+ // }
+ // pi
+ // }
+ Self::ExprObjExtend(ex) => {
+ p!(new: {ex.lhs_work()} str(" ") {ex.rhs_work()})
+ }
+ Self::ExprParened(p) => {
+ p!(new: str("(") {p.expr()} str(")"))
+ }
+ Self::ExprString(s) => p!(new: {s.text()}),
+ Self::ExprNumber(n) => p!(new: {n.number()}),
+ Self::ExprArray(a) => {
+ let mut pi = p!(new: str("[") >i nl);
+ for el in a.exprs() {
+ p!(pi: {el} str(",") nl);
+ }
+ p!(pi: <i str("]"));
+ pi
+ }
+ Self::ExprObject(obj) => {
+ p!(new: {obj.obj_body()})
+ }
+ Self::ExprArrayComp(arr) => {
+ let mut pi = p!(new: str("[") {arr.expr()});
+ for spec in arr.comp_specs() {
+ p!(pi: str(" ") {spec});
}
- p!(pi: items(format_comments(&expr_comments.trivia, CommentLocation::AboveItem)));
- p!(pi: {l.expr()});
+ p!(pi: str("]"));
pi
}
- Expr::ExprIfThenElse(ite) => {
+ Self::ExprImport(v) => {
+ p!(new: {v.import_kind()} str(" ") {v.text()})
+ }
+ Self::ExprVar(n) => p!(new: {n.name()}),
+ // Self::ExprLocal(l) => {
+ // }
+ Self::ExprIfThenElse(ite) => {
let mut pi =
p!(new: str("if ") {ite.cond()} str(" then ") {ite.then().map(|t| t.expr())});
if ite.else_kw_token().is_some() || ite.else_().is_some() {
@@ -541,10 +606,10 @@
}
pi
}
- Expr::ExprFunction(f) => p!(new: str("function") {f.params_desc()} str(" ") {f.expr()}),
- Expr::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),
- Expr::ExprError(e) => p!(new: str("error ") {e.expr()}),
- Expr::ExprLiteral(l) => {
+ Self::ExprFunction(f) => p!(new: str("function") {f.params_desc()} nl {f.expr()}),
+ // Self::ExprAssert(a) => p!(new: {a.assertion()} str("; ") {a.expr()}),
+ Self::ExprError(e) => p!(new: str("error ") {e.expr()}),
+ Self::ExprLiteral(l) => {
p!(new: {l.literal()})
}
}
@@ -575,7 +640,11 @@
}
}
-fn format(input: &str) -> String {
+struct FormatOptions {
+ // 0 for hard tabs
+ indent: u8,
+}
+fn format(input: &str, opts: &FormatOptions) -> Option<String> {
let (parsed, errors) = jrsonnet_rowan_parser::parse(input);
if !errors.is_empty() {
let mut builder = ass_stroke::SnippetBuilder::new(input);
@@ -593,160 +662,143 @@
}
let snippet = builder.build();
let ansi = ass_stroke::source_to_ansi(&snippet);
- println!("{ansi}");
+ eprintln!("{ansi}");
+ // It is possible to recover from this failure, but the output may be broken, as formatter is free to skip
+ // ERROR rowan nodes.
+ // Recovery needs to be enabled for LSP, though.
+ //
+ // TODO: Verify how formatter interacts in cases of missing positional values, i.e `if cond then /*missing Expr*/ else residual`.
+ return None;
}
- dprint_core::formatting::format(
+ Some(dprint_core::formatting::format(
|| parsed.print(),
PrintOptions {
- indent_width: 2,
+ indent_width: if opts.indent == 0 {
+ // Reasonable max length for both 2 and 4 space sized tabs.
+ 3
+ } else {
+ opts.indent
+ },
max_width: 100,
- use_tabs: false,
+ use_tabs: opts.indent == 0,
new_line_text: "\n",
},
- )
+ ))
}
-fn main() {
- let input = r#"
+#[derive(Parser)]
+struct Opts {
+ /// Treat input as code, reformat it instead of reading file.
+ #[clap(long, short = 'e')]
+ exec: bool,
+ /// Path to be reformatted if `--exec` if unset, otherwise code itself.
+ input: String,
+ /// Replace code with formatted in-place, instead of printing it to stdout.
+ /// Only applicable if `--exec` is unset.
+ #[clap(long, short = 'i')]
+ in_place: bool,
- # Edit me!
- local b = import "b.libsonnet"; # comment
- local a = import "a.libsonnet";
+ /// Exit with error if formatted does not match input
+ #[arg(long)]
+ test: bool,
+ /// Number of spaces to indent with
+ ///
+ /// 0 for guess from input (default), and use hard tabs if unable to guess.
+ #[arg(long, default_value = "0")]
+ indent: u8,
+ /// Force hard tab for indentation
+ #[arg(long)]
+ hard_tabs: bool,
- local f(x,y)=x+y;
+ /// Debug option: how many times to call reformatting in case of unstable dprint output resolution.
+ ///
+ /// 0 for not retrying to reformat.
+ #[arg(long, default_value = "0")]
+ conv_limit: usize,
+}
- local {a: [b, ..., c], d, ...e} = null;
+#[derive(thiserror::Error, Debug)]
+enum Error {
+ #[error("--in-place is incompatible with --exec")]
+ InPlaceExec,
+ #[error("io: {0}")]
+ Io(#[from] io::Error),
+ #[error("persist: {0}")]
+ Persist(#[from] tempfile::PersistError),
+ #[error("parsing failed, refusing to reformat corrupted input")]
+ ParseError,
+}
- local ass = assert false : false; false;
-
- local fn = function(a, b, c = 3) 4;
-
- local comp = [a for b in c if d == e];
- local ocomp = {[k]: 1 for k in v};
-
- local ? = skip;
-
- local ie = a[expr];
-
- local unary = !a;
-
- local
- // I am comment
- singleLocalWithItemComment = 1,
- ;
-
- // Comment between local and expression
-
- local
- a = 1, // Inline
- // Comment above b
- b = 4,
-
- // c needs some space
- c = 5,
-
- // Comment after everything
- ;
-
-
- local Template = {z: "foo"};
-
- {
- local
-
- h = 3,
- assert self.a == 1
+fn main_result() -> Result<(), Error> {
+ eprintln!("jrsonnet-fmt is a prototype of a jsonnet code formatter, do not expect it to produce meaningful results right now.");
+ eprintln!("It is not expected for its output to match other implementations, it will be completly separate implementation with maybe different name.");
+ let mut opts = Opts::parse();
+ let input = if opts.exec {
+ if opts.in_place {
+ return Err(Error::InPlaceExec);
+ }
+ opts.input.clone()
+ } else {
+ fs::read_to_string(&opts.input)?
+ };
- : "error",
- "f": ((((((3)))))) ,
- "g g":
- f(4,2),
- arr: [[
- 1, 2,
- ],
- 3,
- {
- b: {
- c: {
- k: [16]
- }
- }
- }
- ],
- m: a[1::],
- m: b[::],
-
- comments: {
- _: '',
- // Plain comment
- a: '',
-
- # Plain comment with empty line before
- b: '',
- /*Single-line multiline comment
-
- */
- c: '',
-
- /**Single-line multiline doc comment
-
- */
- c: '',
-
- /**multiline doc comment
- s
- */
- c: '',
-
- /*
-
- Multi-line
-
- comment
- */
- d: '',
-
- e: '', // Inline comment
-
- k: '',
-
- // Text after everything
- },
- comments2: {
- k: '',
- // Text after everything, but no newline above
- },
- k: if a == b then
-
-
- 2
-
- else Template {},
-
- compspecs: {
- obj_with_no_item: {a:1, for i in [1, 2, 3]},
- obj_with_2_items: {a:1, /*b:2,*/ for i in [1,2,3]},
- }
-
- } + Template
-"#;
+ if opts.indent == 0 {
+ // Sane default.
+ // TODO: Implement actual guessing.
+ opts.hard_tabs = true;
+ }
let mut iteration = 0;
- let mut a = input.to_string();
- let mut b;
+ let mut formatted = input.clone();
+ let mut tmp;
// https://github.com/dprint/dprint/pull/423
loop {
- b = format(&a).trim().to_owned();
- if a == b {
+ let Some(reformatted) = format(
+ &formatted,
+ &FormatOptions {
+ indent: if opts.indent == 0 || opts.hard_tabs {
+ 0
+ } else {
+ opts.indent
+ },
+ },
+ ) else {
+ return Err(Error::ParseError);
+ };
+ tmp = reformatted.trim().to_owned();
+ if formatted == tmp {
break;
}
- println!("{b}");
- a = b;
+ formatted = tmp;
+ if opts.conv_limit == 0 {
+ break;
+ }
iteration += 1;
- if iteration > 5 {
+ if iteration > opts.conv_limit {
panic!("formatting not converged");
- break;
}
}
- println!("{a}");
+ formatted.push('\n');
+ if opts.test && formatted != input {
+ process::exit(1);
+ }
+ if opts.in_place {
+ let path = PathBuf::from(opts.input);
+ let mut temp = tempfile::NamedTempFile::new_in(path.parent().expect(
+ "not failed during read, this path is not a directory, and there is a parent",
+ ))?;
+ temp.write_all(formatted.as_bytes())?;
+ temp.flush()?;
+ temp.persist(&path)?;
+ } else {
+ print!("{formatted}")
+ }
+ Ok(())
+}
+
+fn main() {
+ if let Err(e) = main_result() {
+ eprintln!("{e}");
+ process::exit(1);
+ }
}
cmds/jrsonnet/src/main.rsdiffbeforeafterboth--- a/cmds/jrsonnet/src/main.rs
+++ b/cmds/jrsonnet/src/main.rs
@@ -37,15 +37,15 @@
#[derive(Parser)]
#[clap(next_help_heading = "INPUT")]
struct InputOpts {
- /// Treat input as code, evaluate them instead of reading file
+ /// Treat input as code, evaluate it instead of reading file.
#[clap(long, short = 'e')]
pub exec: bool,
- /// Path to the file to be compiled if `--evaluate` is unset, otherwise code itself
+ /// Path to the file to be compiled if `--exec` is unset, otherwise code itself.
pub input: Option<String>,
/// After executing input, apply specified code.
- /// Output of the initial input will be accessible using `$`
+ /// Output of the initial input will be accessible using `_`.
#[cfg(feature = "exp-apply")]
#[clap(long)]
pub exp_apply: Vec<String>,