1use crate::comment::CommentKind;
8use crate::value::Value;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum Step {
13 Field(String),
15 Index(i64),
17 Iterate,
19 Comment(CommentKind),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum BinOp {
27 Add,
28 Sub,
29 Mul,
30 Div,
31 Mod,
32 Eq,
33 Ne,
34 Lt,
35 Gt,
36 Le,
37 Ge,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub enum Expr {
43 Path(Vec<Step>),
45 Literal(Value),
47 Neg(Box<Expr>),
49 Binary(BinOp, Box<Expr>, Box<Expr>),
51 Pipe(Box<Expr>, Box<Expr>),
53 Alternative(Box<Expr>, Box<Expr>),
56 Comma(Vec<Expr>),
58 Call(String, Vec<Expr>),
60 Collect(Option<Box<Expr>>),
62 ObjectConstruct(Vec<(String, Expr)>),
64 Assign(Box<Expr>, Box<Expr>),
66 UpdateAssign(Box<Expr>, Box<Expr>),
68 AddAssign(Box<Expr>, Box<Expr>),
71}
72
73impl Expr {
74 pub fn is_mutation(&self) -> bool {
77 match self {
78 Expr::Assign(..) | Expr::UpdateAssign(..) | Expr::AddAssign(..) => true,
79 Expr::Call(name, args) => name == "del" || args.iter().any(Expr::is_mutation),
80 Expr::Pipe(a, b) => a.is_mutation() || b.is_mutation(),
81 Expr::Alternative(a, b) => a.is_mutation() || b.is_mutation(),
82 Expr::Comma(items) => items.iter().any(Expr::is_mutation),
83 Expr::Neg(inner) => inner.is_mutation(),
84 Expr::Binary(_, a, b) => a.is_mutation() || b.is_mutation(),
85 Expr::Collect(inner) => inner.as_ref().is_some_and(|e| e.is_mutation()),
86 Expr::ObjectConstruct(pairs) => pairs.iter().any(|(_, e)| e.is_mutation()),
87 Expr::Path(_) | Expr::Literal(_) => false,
88 }
89 }
90
91 pub fn as_path(&self) -> Option<&[Step]> {
94 match self {
95 Expr::Path(steps) => Some(steps),
96 _ => None,
97 }
98 }
99
100 pub fn has_comment(&self) -> bool {
104 match self {
105 Expr::Path(steps) => steps.iter().any(|s| matches!(s, Step::Comment(_))),
106 Expr::Pipe(a, b) | Expr::Alternative(a, b) | Expr::Binary(_, a, b) => {
107 a.has_comment() || b.has_comment()
108 }
109 Expr::Assign(a, b) | Expr::UpdateAssign(a, b) | Expr::AddAssign(a, b) => {
110 a.has_comment() || b.has_comment()
111 }
112 Expr::Comma(items) => items.iter().any(Expr::has_comment),
113 Expr::Neg(inner) => inner.has_comment(),
114 Expr::Call(name, args) => name == "comments" || args.iter().any(Expr::has_comment),
116 Expr::Collect(inner) => inner.as_ref().is_some_and(|e| e.has_comment()),
117 Expr::ObjectConstruct(pairs) => pairs.iter().any(|(_, e)| e.has_comment()),
118 Expr::Literal(_) => false,
119 }
120 }
121}
122
123pub fn render_path(steps: &[Step]) -> String {
126 if steps.is_empty() {
127 return ".".to_string();
128 }
129 let mut out = String::new();
130 for step in steps {
131 match step {
132 Step::Field(k) if is_bare_ident(k) => {
133 out.push('.');
134 out.push_str(k);
135 }
136 Step::Field(k) => out.push_str(&format!(".[{k:?}]")),
139 Step::Index(i) => out.push_str(&format!("[{i}]")),
140 Step::Iterate => out.push_str("[]"),
141 Step::Comment(crate::CommentKind::Head) => out.push_str(".#"),
142 Step::Comment(kind) => out.push_str(&format!(".#.{}", kind.as_str())),
143 }
144 }
145 out
146}
147
148fn is_bare_ident(k: &str) -> bool {
150 let mut chars = k.chars();
151 chars
152 .next()
153 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
154 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn renders_paths() {
163 assert_eq!(render_path(&[]), ".");
164 assert_eq!(
165 render_path(&[Step::Field("a".into()), Step::Field("b".into())]),
166 ".a.b"
167 );
168 assert_eq!(
169 render_path(&[Step::Field("arr".into()), Step::Index(0)]),
170 ".arr[0]"
171 );
172 assert_eq!(
173 render_path(&[Step::Field("xs".into()), Step::Iterate]),
174 ".xs[]"
175 );
176 assert_eq!(render_path(&[Step::Field("a.b".into())]), ".[\"a.b\"]");
178 assert_eq!(
180 render_path(&[Step::Field("a".into()), Step::Comment(CommentKind::Head)]),
181 ".a.#"
182 );
183 assert_eq!(
184 render_path(&[Step::Comment(CommentKind::Inline)]),
185 ".#.inline"
186 );
187 }
188
189 fn p(src: &str) -> Expr {
190 crate::parse(src).unwrap()
191 }
192
193 #[test]
194 fn is_mutation_covers_every_arm() {
195 assert!(p(".a = 1").is_mutation());
196 assert!(p(".a |= . + 1").is_mutation());
197 assert!(p(".a += 1").is_mutation());
198 assert!(p("del(.a)").is_mutation());
199 assert!(p(".a = 1 | .b").is_mutation()); assert!(p(".a = 1, .b").is_mutation()); assert!(p("[.a = 1]").is_mutation()); assert!(p("{k: (.a = 1)}").is_mutation()); assert!(p("select(.a = 1)").is_mutation()); assert!(!p(".a.b[0]").is_mutation());
207 assert!(!p("1 + 2").is_mutation());
208 assert!(!p("keys").is_mutation());
209 assert!(!p("-.a").is_mutation());
210 }
211
212 #[test]
213 fn has_comment_covers_every_arm() {
214 assert!(p(".a.#").has_comment());
215 assert!(p("comments").has_comment());
216 assert!(p(".a.# | ascii_upcase").has_comment()); assert!(p(".a.# // \"x\"").has_comment()); assert!(p(".a.#, .b").has_comment()); assert!(p("[.a.#]").has_comment()); assert!(p("select(.a.#)").has_comment()); assert!(p(".a.# == \"x\"").has_comment()); assert!(!p("-.a").has_comment()); assert!(!p(".a.b").has_comment());
224 }
225
226 #[test]
227 fn as_path_only_for_plain_paths() {
228 assert!(p(".a.b").as_path().is_some());
229 assert!(p("1 + 2").as_path().is_none());
230 assert!(p("keys").as_path().is_none());
231 }
232}