Skip to main content

cedar_policy_formatter/pprint/
utils.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::borrow::Borrow;
18
19use itertools::Itertools;
20use pretty::RcDoc;
21
22use crate::token::regex_constants;
23
24use super::token::{Comment, WrappedToken};
25
26// Add brackets
27pub fn add_brackets<'a>(
28    d: RcDoc<'a>,
29    leftp: RcDoc<'a>,
30    rightp: RcDoc<'a>,
31    offset: isize,
32) -> RcDoc<'a> {
33    leftp.append(d.nest(offset)).append(rightp)
34}
35
36/// Convert a leading comment to an `RcDoc`, adding leading and trailing newlines.
37pub fn get_leading_comment_doc_from_str<'src>(leading_comment: &[&'src str]) -> RcDoc<'src> {
38    if leading_comment.is_empty() {
39        RcDoc::nil()
40    } else {
41        RcDoc::hardline()
42            .append(create_multiline_doc(leading_comment))
43            .append(RcDoc::hardline())
44    }
45}
46
47/// Convert multiline text into an `RcDoc`. Both `RcDoc::as_string` and
48/// `RcDoc::text` allow newlines in the text (although the official
49/// documentation says they don't), but the resulting text will maintain its
50/// original indentation instead of the new "pretty" indentation.
51fn create_multiline_doc<'src>(text: &[&'src str]) -> RcDoc<'src> {
52    RcDoc::intersperse(text.iter().map(|c| RcDoc::text(*c)), RcDoc::hardline())
53}
54
55/// Convert a trailing comment to an `RcDoc`, adding a trailing newline.
56/// There is no need to use `create_multiline_doc` because a trailing comment
57/// cannot contain newlines.
58pub fn get_trailing_comment_doc_from_str<'src>(
59    trailing_comment: &'src str,
60    next_doc: RcDoc<'src>,
61) -> RcDoc<'src> {
62    if !trailing_comment.is_empty() {
63        RcDoc::space()
64            .append(RcDoc::text(trailing_comment))
65            .append(RcDoc::hardline())
66    } else {
67        next_doc
68    }
69}
70
71fn get_token_at_start<'a, 'src>(
72    span: Option<miette::SourceSpan>,
73    tokens: &'a mut [WrappedToken<'src>],
74) -> Option<&'a mut WrappedToken<'src>> {
75    let span = span?;
76    tokens
77        .as_mut()
78        .iter_mut()
79        .find(|t| t.span.start == span.offset())
80}
81
82pub fn get_comment_at_start<'src>(
83    span: Option<miette::SourceSpan>,
84    tokens: &mut [WrappedToken<'src>],
85) -> Option<Comment<'src>> {
86    Some(get_token_at_start(span, tokens)?.consume_comment())
87}
88
89pub fn get_leading_comment_at_start<'src>(
90    span: Option<miette::SourceSpan>,
91    tokens: &mut [WrappedToken<'src>],
92) -> Option<Vec<&'src str>> {
93    Some(get_token_at_start(span, tokens)?.consume_leading_comment())
94}
95
96fn get_token_after_end<'a, 'src>(
97    span: Option<miette::SourceSpan>,
98    tokens: &'a mut [WrappedToken<'src>],
99) -> Option<&'a mut WrappedToken<'src>> {
100    let span = span?;
101    let end = span.offset() + span.len();
102    tokens.iter_mut().find_or_first(|t| t.span.start >= end)
103}
104
105fn get_token_at_end<'a, 'src>(
106    span: Option<miette::SourceSpan>,
107    tokens: &'a mut [WrappedToken<'src>],
108) -> Option<&'a mut WrappedToken<'src>> {
109    let span = span?;
110    let end = span.offset() + span.len();
111    tokens.iter_mut().find(|t| t.span.end == end)
112}
113
114pub fn get_comment_at_end<'src>(
115    span: Option<miette::SourceSpan>,
116    tokens: &mut [WrappedToken<'src>],
117) -> Option<Comment<'src>> {
118    Some(get_token_at_end(span, tokens)?.consume_comment())
119}
120
121pub fn get_comment_after_end<'src>(
122    span: Option<miette::SourceSpan>,
123    tokens: &mut [WrappedToken<'src>],
124) -> Option<Comment<'src>> {
125    Some(get_token_after_end(span, tokens)?.consume_comment())
126}
127
128pub fn get_comment_in_range<'src>(
129    span: Option<miette::SourceSpan>,
130    tokens: &mut [WrappedToken<'src>],
131) -> Option<Vec<Comment<'src>>> {
132    let span = span?;
133    Some(
134        tokens
135            .iter_mut()
136            .skip_while(|t| t.span.start < span.offset())
137            .take_while(|t| t.span.end <= span.offset() + span.len())
138            .map(|t| t.consume_comment())
139            .collect(),
140    )
141}
142
143/// Wrap an `RcDoc` with comments. If there is a leading comment, then this
144/// will introduce a newline bat the start of the `RcDoc`. If there is a
145/// trailing comment, then it will introduce a newline at the end.
146pub fn add_comment<'src>(
147    d: RcDoc<'src>,
148    comment: impl Borrow<Comment<'src>>,
149    next_doc: RcDoc<'src>,
150) -> RcDoc<'src> {
151    let leading_comment = comment.borrow().leading_comment();
152    let trailing_comment = comment.borrow().trailing_comment();
153    let leading_comment_doc = get_leading_comment_doc_from_str(leading_comment);
154    let trailing_comment_doc = get_trailing_comment_doc_from_str(trailing_comment, next_doc);
155    leading_comment_doc.append(d).append(trailing_comment_doc)
156}
157
158/// Remove empty lines from the input string, ignoring the first and last lines.
159/// (Because of how this function is used in `remove_empty_lines`, the first and
160/// last lines may include important spacing information.) This will remove empty
161/// lines  _everywhere_, including in places where that may not be desired
162/// (e.g., in string literals).
163fn remove_empty_interior_lines(s: &str) -> String {
164    let mut new_s = String::new();
165    if s.starts_with('\n') {
166        new_s.push('\n');
167    }
168    new_s.push_str(
169        s.split_inclusive('\n')
170            // in the case where `s` does not end in a newline, `!ss.contains('\n')`
171            // preserves whitespace on the last line
172            .filter(|ss| !ss.trim().is_empty() || !ss.contains('\n'))
173            .collect::<Vec<_>>()
174            .join("")
175            .as_str(),
176    );
177    new_s
178}
179
180/// Remove empty lines, safely handling newlines that occur in quotations.
181pub fn remove_empty_lines(text: &str) -> String {
182    let mut index = 0;
183    let mut final_text = String::new();
184
185    while index < text.len() {
186        // Check for the next comment and string. The general strategy is to
187        // call `remove_empty_interior_lines` on all the text _outside_ of
188        // strings. Comments should be skipped to avoid interpreting a quote in
189        // a comment as a string.
190        let comment_match = regex_constants::COMMENT.find_at(text, index);
191        let string_match = regex_constants::STRING.find_at(text, index);
192        match (comment_match, string_match) {
193            (Some(m1), Some(m2)) => {
194                // Handle the earlier match
195                let m = std::cmp::min_by_key(m1, m2, |m| m.start());
196                #[expect(
197                    clippy::string_slice,
198                    reason = "Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`."
199                )]
200                final_text.push_str(&remove_empty_interior_lines(&text[index..m.start()]));
201                final_text.push_str(m.as_str());
202                index = m.end();
203            }
204            (Some(m), None) | (None, Some(m)) => {
205                #[expect(
206                    clippy::string_slice,
207                    reason = "Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`."
208                )]
209                final_text.push_str(&remove_empty_interior_lines(&text[index..m.start()]));
210                final_text.push_str(m.as_str());
211                index = m.end();
212            }
213            (None, None) => {
214                #[expect(
215                    clippy::string_slice,
216                    reason = "Slicing `text` is safe since `index` is within the bounds of `text`."
217                )]
218                final_text.push_str(&remove_empty_interior_lines(&text[index..]));
219                break;
220            }
221        }
222    }
223    // Trim the final result to account for dangling newlines
224    final_text.trim().to_string()
225}