forge_fmt/
inline_config.rs

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