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.next().map(|(idx, _)| idx).unwrap_or_default();
103
104                    let end_offset = loc.end();
105                    let mut next_newline =
106                        src[end_offset..].char_indices().skip_while(|(_, ch)| *ch != '\n');
107                    let end =
108                        end_offset + next_newline.next().map(|(idx, _)| idx).unwrap_or_default();
109
110                    disabled_ranges.push(DisabledRange { start, end, loose: false });
111                }
112                InlineConfigItem::DisableNextLine => {
113                    let offset = loc.end();
114                    let mut char_indices =
115                        src[offset..].char_indices().skip_while(|(_, ch)| *ch != '\n').skip(1);
116                    if let Some((mut start, _)) = char_indices.next() {
117                        start += offset;
118                        let end = char_indices
119                            .find(|(_, ch)| *ch == '\n')
120                            .map(|(idx, _)| offset + idx + 1)
121                            .unwrap_or(src.len());
122                        disabled_ranges.push(DisabledRange { start, end, loose: false });
123                    }
124                }
125                InlineConfigItem::DisableStart => {
126                    if disabled_depth == 0 {
127                        disabled_range_start = Some(loc.end());
128                    }
129                    disabled_depth += 1;
130                }
131                InlineConfigItem::DisableEnd => {
132                    disabled_depth = disabled_depth.saturating_sub(1);
133                    if disabled_depth == 0 {
134                        if let Some(start) = disabled_range_start.take() {
135                            disabled_ranges.push(DisabledRange {
136                                start,
137                                end: loc.start(),
138                                loose: false,
139                            })
140                        }
141                    }
142                }
143            }
144        }
145        if let Some(start) = disabled_range_start.take() {
146            disabled_ranges.push(DisabledRange { start, end: src.len(), loose: false })
147        }
148        Self { disabled_ranges }
149    }
150
151    /// Check if the location is in a disabled range
152    pub fn is_disabled(&self, loc: Loc) -> bool {
153        self.disabled_ranges.iter().any(|range| range.includes(loc))
154    }
155}