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 DocSelect(usize, Box<Expr>),
76}
77
78impl Expr {
79 pub fn is_mutation(&self) -> bool {
82 match self {
83 Expr::Assign(..) | Expr::UpdateAssign(..) | Expr::AddAssign(..) => true,
84 Expr::Call(name, args) => name == "del" || args.iter().any(Expr::is_mutation),
85 Expr::Pipe(a, b) => a.is_mutation() || b.is_mutation(),
86 Expr::Alternative(a, b) => a.is_mutation() || b.is_mutation(),
87 Expr::Comma(items) => items.iter().any(Expr::is_mutation),
88 Expr::Neg(inner) => inner.is_mutation(),
89 Expr::Binary(_, a, b) => a.is_mutation() || b.is_mutation(),
90 Expr::Collect(inner) => inner.as_ref().is_some_and(|e| e.is_mutation()),
91 Expr::ObjectConstruct(pairs) => pairs.iter().any(|(_, e)| e.is_mutation()),
92 Expr::DocSelect(_, body) => body.is_mutation(),
93 Expr::Path(_) | Expr::Literal(_) => false,
94 }
95 }
96
97 pub fn as_path(&self) -> Option<&[Step]> {
100 match self {
101 Expr::Path(steps) => Some(steps),
102 _ => None,
103 }
104 }
105
106 pub fn has_comment(&self) -> bool {
110 match self {
111 Expr::Path(steps) => steps.iter().any(|s| matches!(s, Step::Comment(_))),
112 Expr::Pipe(a, b) | Expr::Alternative(a, b) | Expr::Binary(_, a, b) => {
113 a.has_comment() || b.has_comment()
114 }
115 Expr::Assign(a, b) | Expr::UpdateAssign(a, b) | Expr::AddAssign(a, b) => {
116 a.has_comment() || b.has_comment()
117 }
118 Expr::Comma(items) => items.iter().any(Expr::has_comment),
119 Expr::Neg(inner) => inner.has_comment(),
120 Expr::Call(name, args) => name == "comments" || args.iter().any(Expr::has_comment),
122 Expr::Collect(inner) => inner.as_ref().is_some_and(|e| e.has_comment()),
123 Expr::ObjectConstruct(pairs) => pairs.iter().any(|(_, e)| e.has_comment()),
124 Expr::DocSelect(_, body) => body.has_comment(),
125 Expr::Literal(_) => false,
126 }
127 }
128}
129
130pub fn render_path(steps: &[Step]) -> String {
133 if steps.is_empty() {
134 return ".".to_string();
135 }
136 let mut out = String::new();
137 for step in steps {
138 match step {
139 Step::Field(k) if is_bare_ident(k) => {
140 out.push('.');
141 out.push_str(k);
142 }
143 Step::Field(k) => out.push_str(&format!(".[{k:?}]")),
146 Step::Index(i) => out.push_str(&format!("[{i}]")),
147 Step::Iterate => out.push_str("[]"),
148 Step::Comment(crate::CommentKind::Head) => out.push_str(".#"),
149 Step::Comment(kind) => out.push_str(&format!(".#.{}", kind.as_str())),
150 }
151 }
152 out
153}
154
155fn is_bare_ident(k: &str) -> bool {
157 let mut chars = k.chars();
158 chars
159 .next()
160 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
161 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn renders_paths() {
170 assert_eq!(render_path(&[]), ".");
171 assert_eq!(
172 render_path(&[Step::Field("a".into()), Step::Field("b".into())]),
173 ".a.b"
174 );
175 assert_eq!(
176 render_path(&[Step::Field("arr".into()), Step::Index(0)]),
177 ".arr[0]"
178 );
179 assert_eq!(
180 render_path(&[Step::Field("xs".into()), Step::Iterate]),
181 ".xs[]"
182 );
183 assert_eq!(render_path(&[Step::Field("a.b".into())]), ".[\"a.b\"]");
185 assert_eq!(
187 render_path(&[Step::Field("a".into()), Step::Comment(CommentKind::Head)]),
188 ".a.#"
189 );
190 assert_eq!(
191 render_path(&[Step::Comment(CommentKind::Inline)]),
192 ".#.inline"
193 );
194 }
195
196 fn p(src: &str) -> Expr {
197 crate::parse(src).unwrap()
198 }
199
200 #[test]
201 fn is_mutation_covers_every_arm() {
202 assert!(p(".a = 1").is_mutation());
203 assert!(p(".a |= . + 1").is_mutation());
204 assert!(p(".a += 1").is_mutation());
205 assert!(p("del(.a)").is_mutation());
206 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());
214 assert!(!p("1 + 2").is_mutation());
215 assert!(!p("keys").is_mutation());
216 assert!(!p("-.a").is_mutation());
217 }
218
219 #[test]
220 fn has_comment_covers_every_arm() {
221 assert!(p(".a.#").has_comment());
222 assert!(p("comments").has_comment());
223 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());
231 }
232
233 #[test]
234 fn as_path_only_for_plain_paths() {
235 assert!(p(".a.b").as_path().is_some());
236 assert!(p("1 + 2").as_path().is_none());
237 assert!(p("keys").as_path().is_none());
238 }
239}