solang_forge_fmt/
inline_config.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::comments::{CommentState, CommentStringExt};
4use itertools::Itertools;
5use solang_parser::pt::Loc;
6use std::{fmt, str::FromStr};
7
8/// An inline config item
9#[allow(clippy::enum_variant_names)]
10#[derive(Clone, Copy, Debug)]
11pub enum InlineConfigItem {
12    /// Disables the next code item regardless of newlines
13    DisableNextItem,
14    /// Disables formatting on the current line
15    DisableLine,
16    /// Disables formatting between the next newline and the newline after
17    DisableNextLine,
18    /// Disables formatting for any code that follows this and before the next "disable-end"
19    DisableStart,
20    /// Disables formatting for any code that precedes this and after the previous "disable-start"
21    DisableEnd,
22}
23
24impl FromStr for InlineConfigItem {
25    type Err = InvalidInlineConfigItem;
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        Ok(match s {
28            "disable-next-item" => Self::DisableNextItem,
29            "disable-line" => Self::DisableLine,
30            "disable-next-line" => Self::DisableNextLine,
31            "disable-start" => Self::DisableStart,
32            "disable-end" => Self::DisableEnd,
33            s => return Err(InvalidInlineConfigItem(s.into())),
34        })
35    }
36}
37
38#[derive(Debug)]
39pub struct InvalidInlineConfigItem(String);
40
41impl fmt::Display for InvalidInlineConfigItem {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_fmt(format_args!("Invalid inline config item: {}", self.0))
44    }
45}
46
47/// A disabled formatting range. `loose` designates that the range includes any loc which
48/// may start in between start and end, whereas the strict version requires that
49/// `range.start >= loc.start <=> loc.end <= range.end`
50#[derive(Debug)]
51struct DisabledRange {
52    start: usize,
53    end: usize,
54    loose: bool,
55}
56
57impl DisabledRange {
58    fn includes(&self, loc: Loc) -> bool {
59        loc.start() >= self.start && (if self.loose { loc.start() } else { loc.end() } <= self.end)
60    }
61}
62
63/// An inline config. Keeps track of disabled ranges.
64///
65/// This is a list of Inline Config items for locations in a source file. This is
66/// usually acquired by parsing the comments for an `forgefmt:` items.
67///
68/// See [`Comments::parse_inline_config_items`](crate::Comments::parse_inline_config_items) for
69/// details.
70#[derive(Debug, Default)]
71pub struct InlineConfig {
72    disabled_ranges: Vec<DisabledRange>,
73}
74
75impl InlineConfig {
76    /// Build a new inline config with an iterator of inline config items and their locations in a
77    /// source file
78    pub fn new(items: impl IntoIterator<Item = (Loc, InlineConfigItem)>, src: &str) -> Self {
79        let mut disabled_ranges = vec![];
80        let mut disabled_range_start = None;
81        let mut disabled_depth = 0usize;
82        for (loc, item) in items.into_iter().sorted_by_key(|(loc, _)| loc.start()) {
83            match item {
84                InlineConfigItem::DisableNextItem => {
85                    let offset = loc.end();
86                    let mut char_indices = src[offset..]
87                        .comment_state_char_indices()
88                        .filter_map(|(state, idx, ch)| match state {
89                            CommentState::None => Some((idx, ch)),
90                            _ => None,
91                        })
92                        .skip_while(|(_, ch)| ch.is_whitespace());
93                    if let Some((mut start, _)) = char_indices.next() {
94                        start += offset;
95                        let end = char_indices
96                            .find(|(_, ch)| !ch.is_whitespace())
97                            .map(|(idx, _)| offset + idx)
98                            .unwrap_or(src.len());
99                        disabled_ranges.push(DisabledRange {
100                            start,
101                            end,
102                            loose: true,
103                        });
104                    }
105                }
106                InlineConfigItem::DisableLine => {
107                    let mut prev_newline = src[..loc.start()]
108                        .char_indices()
109                        .rev()
110                        .skip_while(|(_, ch)| *ch != '\n');
111                    let start = prev_newline.next().map(|(idx, _)| idx).unwrap_or_default();
112
113                    let end_offset = loc.end();
114                    let mut next_newline = src[end_offset..]
115                        .char_indices()
116                        .skip_while(|(_, ch)| *ch != '\n');
117                    let end =
118                        end_offset + next_newline.next().map(|(idx, _)| idx).unwrap_or_default();
119
120                    disabled_ranges.push(DisabledRange {
121                        start,
122                        end,
123                        loose: false,
124                    });
125                }
126                InlineConfigItem::DisableNextLine => {
127                    let offset = loc.end();
128                    let mut char_indices = src[offset..]
129                        .char_indices()
130                        .skip_while(|(_, ch)| *ch != '\n')
131                        .skip(1);
132                    if let Some((mut start, _)) = char_indices.next() {
133                        start += offset;
134                        let end = char_indices
135                            .find(|(_, ch)| *ch == '\n')
136                            .map(|(idx, _)| offset + idx + 1)
137                            .unwrap_or(src.len());
138                        disabled_ranges.push(DisabledRange {
139                            start,
140                            end,
141                            loose: false,
142                        });
143                    }
144                }
145                InlineConfigItem::DisableStart => {
146                    if disabled_depth == 0 {
147                        disabled_range_start = Some(loc.end());
148                    }
149                    disabled_depth += 1;
150                }
151                InlineConfigItem::DisableEnd => {
152                    disabled_depth = disabled_depth.saturating_sub(1);
153                    if disabled_depth == 0 {
154                        if let Some(start) = disabled_range_start.take() {
155                            disabled_ranges.push(DisabledRange {
156                                start,
157                                end: loc.start(),
158                                loose: false,
159                            })
160                        }
161                    }
162                }
163            }
164        }
165        if let Some(start) = disabled_range_start.take() {
166            disabled_ranges.push(DisabledRange {
167                start,
168                end: src.len(),
169                loose: false,
170            })
171        }
172        Self { disabled_ranges }
173    }
174
175    /// Check if the location is in a disabled range
176    pub fn is_disabled(&self, loc: Loc) -> bool {
177        self.disabled_ranges.iter().any(|range| range.includes(loc))
178    }
179}