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