cedar_policy_formatter/pprint/
fmt.rs1use std::collections::BTreeMap;
18
19use miette::{miette, Result, WrapErr};
20
21use cedar_policy_core::ast::PolicySet;
22use cedar_policy_core::parser::parse_policyset;
23use cedar_policy_core::parser::text_to_cst::parse_policies;
24use smol_str::ToSmolStr;
25
26use super::lexer::get_token_stream;
27use super::utils::remove_empty_lines;
28
29use super::config::{self, Config};
30use super::doc::*;
31
32fn tree_to_pretty<T: Doc>(t: &T, context: &mut config::Context<'_, '_>) -> Result<String> {
33 let mut w = Vec::new();
34 let config = context.config;
35 let doc = t.to_doc(context);
36 doc.ok_or_else(|| miette!("failed to produce doc"))?
37 .render(config.line_width, &mut w)
38 .map_err(|err| miette!(format!("failed to render doc: {err}")))?;
39 String::from_utf8(w)
40 .map_err(|err| miette!(format!("failed to convert rendered doc to string: {err}")))
41}
42
43fn soundness_check(ps: &str, ast: &PolicySet) -> Result<()> {
44 let formatted_ast =
45 parse_policyset(ps).wrap_err(format!("formatter produced an invalid policy set:\n{ps}"))?;
46 let (formatted_policies, policies) = (
47 formatted_ast
48 .policies()
49 .map(|p| (p.id().to_smolstr(), p))
50 .collect::<BTreeMap<_, _>>(),
51 ast.policies()
52 .map(|p| (p.id().to_smolstr(), p))
53 .collect::<BTreeMap<_, _>>(),
54 );
55
56 if formatted_policies.len() != policies.len() {
57 return Err(miette!(
58 "formatter changed the number of policies from {} to {}",
59 policies.len(),
60 formatted_policies.len()
61 ));
62 }
63 for ((f_p_id, f_p), (p_id, p)) in formatted_policies.into_iter().zip(policies.into_iter()) {
64 if f_p_id != p_id {
65 return Err(miette!(
66 "formatter changed the policy id from {p_id} to {f_p_id}"
67 ));
68 }
69 let (f_anno, anno) = (
70 f_p.annotations()
71 .map(|(k, v)| (k, &v.val))
72 .collect::<std::collections::BTreeMap<_, _>>(),
73 p.annotations()
74 .map(|(k, v)| (k, &v.val))
75 .collect::<std::collections::BTreeMap<_, _>>(),
76 );
77 if f_anno != anno {
78 return Err(miette!(
79 "formatter changed the annotations from {anno:?} to {f_anno:?}"
80 ));
81 }
82 if !(f_p.effect() == p.effect()
83 && f_p.principal_constraint() == p.principal_constraint()
84 && f_p.action_constraint() == p.action_constraint()
85 && f_p.resource_constraint() == p.resource_constraint()
86 && f_p
87 .non_scope_constraints()
88 .eq_shape(p.non_scope_constraints()))
89 {
90 return Err(miette!(
91 "formatter changed the policy structure:\noriginal:\n{p}\nformatted:\n{f_p}"
92 ));
93 }
94 }
95 Ok(())
96}
97
98pub fn policies_str_to_pretty(ps: &str, config: &Config) -> Result<String> {
99 let cst = parse_policies(ps).wrap_err("cannot parse input policies")?;
100 let ast = cst.to_policyset().wrap_err("cannot parse input policies")?;
101 let (tokens, end_of_file_comment) =
102 get_token_stream(ps).ok_or_else(|| miette!("cannot get token stream"))?;
103 let mut context = config::Context { config, tokens };
104 let mut formatted_policies = cst
105 .as_inner()
106 .ok_or_else(|| miette!("fail to get input policy CST"))?
107 .0
108 .iter()
109 .map(|p| Ok(remove_empty_lines(&tree_to_pretty(p, &mut context)?)))
110 .collect::<Result<Vec<String>>>()?
111 .join("\n\n");
112
113 formatted_policies.push('\n');
115
116 for comment_line in end_of_file_comment {
118 formatted_policies.push_str(comment_line);
119 formatted_policies.push('\n');
121 }
122
123 soundness_check(&formatted_policies, &ast).wrap_err(
125 "internal error: please file an issue at <https://github.com/cedar-policy/cedar/issues>",
126 )?;
127 Ok(formatted_policies)
128}
129
130#[cfg(test)]
131mod tests {
132 use insta::{assert_snapshot, glob, with_settings};
133 use std::fs;
134
135 use super::*;
136
137 #[test]
138 fn test_soundness_check() {
139 let p1 = r#"permit (principal, action, resource)
140 when { "
141
142 a
143 " };"#;
144 let p2 = r#"permit (principal, action, resource)
145 when { "
146 a
147 " };"#;
148 assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_err());
149
150 let p1 = r#"
151 permit (principal, action, resource)
152 when { "a"};
153 permit (principal, action, resource)
154 when { "
155
156 a
157 " };"#;
158 let p2 = r#"
159 permit (principal, action, resource)
160 when { "
161 a
162 " };
163 permit (principal, action, resource)
164 when { "a"};"#;
165 assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_err());
166
167 let p1 = r#"
168 permit (principal, action, resource)
169 when { "a" };
170 permit (principal, action, resource)
171 when { "b" };"#;
172 let p2 = r#"
173 permit (principal, action, resource)
174 when { "a" };
175 permit (principal, action, resource)
176 when { "b"};"#;
177 assert!(soundness_check(p2, &parse_policyset(p1).unwrap()).is_ok());
178 }
179
180 #[test]
181 fn test_add_trailing_newline() {
182 let config = Config {
187 line_width: 80,
188 indent_width: 2,
189 };
190
191 let formatted_p = "permit (principal, action, resource);\n";
192 let p1 = "permit (principal, action, resource);";
193 let p2 = "permit (principal, action, resource);\r\n";
194 let p3 = "permit (principal, action, resource);\n\r\n\n";
195
196 assert_eq!(
197 policies_str_to_pretty(formatted_p, &config).unwrap(),
198 formatted_p
199 );
200 assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
201 assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
202 assert_eq!(policies_str_to_pretty(p3, &config).unwrap(), formatted_p);
203
204 let formatted_p = "permit (principal, action, resource);\n//foo\n";
205 let p1 = "permit (principal, action, resource);\n//foo";
206 let p2 = "permit (principal, action, resource);\n//foo\n\n\n";
207
208 assert_eq!(
209 policies_str_to_pretty(formatted_p, &config).unwrap(),
210 formatted_p
211 );
212 assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
213 assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
214
215 let formatted_p = "permit (principal, action, resource);\n//foo\n//bar\n";
216 let p1 = "permit (principal, action, resource);\n//foo\n//bar";
217 let p2 = "permit (principal, action, resource);\n//foo\n//bar ";
218 let p3 = "permit (principal, action, resource);\n//foo\n//bar\n\n\n";
219 let p4 = "permit (principal, action, resource);\n//foo\n//bar \n\n\n";
220 let p5 = "permit (principal, action, resource);\n//foo\n//bar \n \n \n";
221
222 assert_eq!(
223 policies_str_to_pretty(formatted_p, &config).unwrap(),
224 formatted_p
225 );
226 assert_eq!(policies_str_to_pretty(p1, &config).unwrap(), formatted_p);
227 assert_eq!(policies_str_to_pretty(p2, &config).unwrap(), formatted_p);
228 assert_eq!(policies_str_to_pretty(p3, &config).unwrap(), formatted_p);
229 assert_eq!(policies_str_to_pretty(p4, &config).unwrap(), formatted_p);
230 assert_eq!(policies_str_to_pretty(p5, &config).unwrap(), formatted_p);
231 }
232
233 #[test]
234 fn test_format_files() {
235 let config = Config {
236 line_width: 80,
237 indent_width: 2,
238 };
239
240 with_settings!(
253 { snapshot_path => "../../tests/snapshots/" },
254 {
255 glob!("../../tests", "*.cedar", |path| {
256 let cedar_source = fs::read_to_string(path).unwrap();
257 let formatted = policies_str_to_pretty(&cedar_source, &config).unwrap();
258 assert_snapshot!(formatted);
259 });
260 }
261 );
262
263 with_settings!(
265 { snapshot_path => "../../tests/cli-snapshots/" },
266 {
267 glob!("../../../cedar-policy-cli/sample-data", "**/*.cedar", |path| {
268 let cedar_source = fs::read_to_string(path).unwrap();
269 let formatted = policies_str_to_pretty(&cedar_source, &config).unwrap();
270 assert_snapshot!(formatted);
271 });
272 }
273 )
274 }
275}