Skip to content

Commit cce1a86

Browse files
committed
feat: add scopelint: ignore directive
1 parent 82897bf commit cce1a86

5 files changed

Lines changed: 159 additions & 6 deletions

File tree

src/check/inline_config.rs

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ pub enum InlineConfigItem {
2323
DisableStart,
2424
/// Disables formatting for any code that precedes this and after the previous "disable-start"
2525
DisableEnd,
26+
/// Ignores the next code item for linting rules
27+
IgnoreNextItem,
28+
/// Ignores the current line for linting rules
29+
IgnoreLine,
30+
/// Ignores the next line for linting rules
31+
IgnoreNextLine,
32+
/// Ignores linting rules for any code that follows this and before the next "ignore-end"
33+
IgnoreStart,
34+
/// Ignores linting rules for any code that precedes this and after the previous "ignore-start"
35+
IgnoreEnd,
2636
}
2737

2838
impl FromStr for InlineConfigItem {
@@ -34,6 +44,11 @@ impl FromStr for InlineConfigItem {
3444
"disable-next-line" => InlineConfigItem::DisableNextLine,
3545
"disable-start" => InlineConfigItem::DisableStart,
3646
"disable-end" => InlineConfigItem::DisableEnd,
47+
"ignore-next-item" => InlineConfigItem::IgnoreNextItem,
48+
"ignore-line" => InlineConfigItem::IgnoreLine,
49+
"ignore-next-line" => InlineConfigItem::IgnoreNextLine,
50+
"ignore-start" => InlineConfigItem::IgnoreStart,
51+
"ignore-end" => InlineConfigItem::IgnoreEnd,
3752
s => return Err(InvalidInlineConfigItem(s.into())),
3853
})
3954
}
@@ -64,23 +79,45 @@ impl DisabledRange {
6479
}
6580
}
6681

67-
/// An inline config. Keeps track of disabled ranges.
68-
///
82+
/// An ignored formatting range. `loose` designates that the range includes any loc which
83+
/// may start in between start and end, whereas the strict version requires that
84+
/// `range.start >= loc.start <=> loc.end <= range.end`
85+
#[derive(Debug)]
86+
struct IgnoredRange {
87+
start: usize,
88+
end: usize,
89+
loose: bool,
90+
}
91+
92+
impl IgnoredRange {
93+
fn includes(&self, loc: Loc) -> bool {
94+
loc.start() >= self.start && (if self.loose { loc.start() } else { loc.end() } <= self.end)
95+
}
96+
}
97+
6998
/// This is a list of Inline Config items for locations in a source file. This is acquired by
7099
/// parsing the comments for `scopelint:` items. See [`Comments::parse_inline_config_items`] for
71100
/// details.
72101
#[derive(Default, Debug)]
73102
pub struct InlineConfig {
74103
disabled_ranges: Vec<DisabledRange>,
104+
ignored_ranges: Vec<IgnoredRange>,
75105
}
76106

77107
impl InlineConfig {
78108
/// Build a new inline config with an iterator of inline config items and their locations in a
79109
/// source file
80110
pub fn new(items: impl IntoIterator<Item = (Loc, InlineConfigItem)>, src: &str) -> Self {
111+
// Disable ranges (for formatting)
81112
let mut disabled_ranges = vec![];
82113
let mut disabled_range_start = None;
83114
let mut disabled_depth = 0usize;
115+
116+
// Ignore ranges (for linting)
117+
let mut ignored_ranges = vec![];
118+
let mut ignored_range_start = None;
119+
let mut ignored_depth = 0usize;
120+
84121
for (loc, item) in items.into_iter().sorted_by_key(|(loc, _)| loc.start()) {
85122
match item {
86123
InlineConfigItem::DisableNextItem => {
@@ -145,16 +182,99 @@ impl InlineConfig {
145182
}
146183
}
147184
}
185+
InlineConfigItem::IgnoreNextItem => {
186+
let offset = loc.end();
187+
let mut char_indices = src[offset..]
188+
.comment_state_char_indices()
189+
.filter_map(|(state, idx, ch)| match state {
190+
CommentState::None => Some((idx, ch)),
191+
_ => None,
192+
})
193+
.skip_while(|(_, ch)| ch.is_whitespace());
194+
if let Some((mut start, _)) = char_indices.next() {
195+
start += offset;
196+
// Find the end of the function declaration by looking for the closing brace
197+
let mut brace_count = 0;
198+
let mut found_function_start = false;
199+
let mut end = src.len();
200+
201+
for (idx, ch) in src[start..].char_indices() {
202+
if ch == '{' {
203+
brace_count += 1;
204+
found_function_start = true;
205+
} else if ch == '}' {
206+
brace_count -= 1;
207+
if found_function_start && brace_count == 0 {
208+
end = start + idx + 1;
209+
break;
210+
}
211+
}
212+
}
213+
ignored_ranges.push(IgnoredRange { start, end, loose: true });
214+
}
215+
}
216+
InlineConfigItem::IgnoreLine => {
217+
let mut prev_newline =
218+
src[..loc.start()].char_indices().rev().skip_while(|(_, ch)| *ch != '\n');
219+
let start = prev_newline.next().map(|(idx, _)| idx).unwrap_or_default();
220+
221+
let end_offset = loc.end();
222+
let mut next_newline =
223+
src[end_offset..].char_indices().skip_while(|(_, ch)| *ch != '\n');
224+
let end =
225+
end_offset + next_newline.next().map(|(idx, _)| idx).unwrap_or_default();
226+
227+
ignored_ranges.push(IgnoredRange { start, end, loose: false });
228+
}
229+
InlineConfigItem::IgnoreNextLine => {
230+
let offset = loc.end();
231+
let mut char_indices =
232+
src[offset..].char_indices().skip_while(|(_, ch)| *ch != '\n').skip(1);
233+
if let Some((mut start, _)) = char_indices.next() {
234+
start += offset;
235+
let end = char_indices
236+
.find(|(_, ch)| *ch == '\n')
237+
.map(|(idx, _)| offset + idx + 1)
238+
.unwrap_or(src.len());
239+
ignored_ranges.push(IgnoredRange { start, end, loose: false });
240+
}
241+
}
242+
InlineConfigItem::IgnoreStart => {
243+
if ignored_depth == 0 {
244+
ignored_range_start = Some(loc.end());
245+
}
246+
ignored_depth += 1;
247+
}
248+
InlineConfigItem::IgnoreEnd => {
249+
ignored_depth = ignored_depth.saturating_sub(1);
250+
if ignored_depth == 0 {
251+
if let Some(start) = ignored_range_start.take() {
252+
ignored_ranges.push(IgnoredRange {
253+
start,
254+
end: loc.start(),
255+
loose: false,
256+
})
257+
}
258+
}
259+
}
148260
}
149261
}
150262
if let Some(start) = disabled_range_start.take() {
151263
disabled_ranges.push(DisabledRange { start, end: src.len(), loose: false })
152264
}
153-
Self { disabled_ranges }
265+
if let Some(start) = ignored_range_start.take() {
266+
ignored_ranges.push(IgnoredRange { start, end: src.len(), loose: false })
267+
}
268+
Self { disabled_ranges, ignored_ranges }
154269
}
155270

156271
/// Check if the location is in a disabled range
157272
pub fn is_disabled(&self, loc: Loc) -> bool {
158273
self.disabled_ranges.iter().any(|range| range.includes(loc))
159274
}
275+
276+
/// Check if the location is in an ignored range
277+
pub fn is_ignored(&self, loc: Loc) -> bool {
278+
self.ignored_ranges.iter().any(|range| range.includes(loc))
279+
}
160280
}

src/check/report.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ impl fmt::Display for Report {
1313
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
1414
self.invalid_items
1515
.iter()
16-
.filter(|item| !item.is_disabled)
16+
.filter(|item| !item.is_disabled && !item.is_ignored)
1717
.sorted_unstable()
1818
.try_for_each(|item| writeln!(f, "{}", item.description()))
1919
}
@@ -33,6 +33,6 @@ impl Report {
3333
/// Returns true if no issues were found.
3434
#[must_use]
3535
pub fn is_valid(&self) -> bool {
36-
!self.invalid_items.iter().any(|item| !item.is_disabled)
36+
!self.invalid_items.iter().any(|item| !item.is_disabled && !item.is_ignored)
3737
}
3838
}

src/check/utils.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub struct InvalidItem {
3636
pub text: String, // Details to show about the invalid item.
3737
pub line: usize, // Line number.
3838
pub is_disabled: bool, // Whether the invalid item is in a disabled region.
39+
pub is_ignored: bool, // Whether the invalid item is in an ignored region.
3940
}
4041

4142
impl InvalidItem {
@@ -45,7 +46,8 @@ impl InvalidItem {
4546
let Parsed { file, src, inline_config, .. } = parsed;
4647
let line = offset_to_line(src, loc.start());
4748
let is_disabled = inline_config.is_disabled(loc);
48-
Self { kind, file: file.display().to_string(), text, line, is_disabled }
49+
let is_ignored = inline_config.is_ignored(loc);
50+
Self { kind, file: file.display().to_string(), text, line, is_disabled, is_ignored }
4951
}
5052

5153
#[must_use]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Test file showing differences between ignore directives
2+
pragma solidity ^0.8.17;
3+
4+
contract CounterIgnored4 {
5+
// Test 1: ignore-next-item (ignores entire function declaration, even multiline)
6+
// scopelint: ignore-next-item
7+
function multiLineFunction(
8+
address user,
9+
uint256 amount
10+
) internal {
11+
// complex function body
12+
}
13+
14+
// Test 2: ignore-next-line (ignores only the next line)
15+
// scopelint: ignore-next-line
16+
function singleLineFunction() internal {}
17+
18+
// Test 3: ignore-line (ignores only this comment line, NOT the function)
19+
function functionOnSameLine() internal {} // scopelint: ignore-line
20+
21+
// Test 4: ignore-start/ignore-end (ignores multiple items)
22+
// scopelint: ignore-start
23+
function batchFunction1() internal {}
24+
function batchFunction2() private {}
25+
function batchFunction3() internal {}
26+
// scopelint: ignore-end
27+
28+
// Control test: this should be flagged (no ignore directive)
29+
function missingLeadingUnderscoreAndNotIgnored() internal {}
30+
}

tests/check.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fn test_check_proj1_all_findings() {
3434
"Invalid constant or immutable name in ./test/Counter.t.sol on line 7: testVal",
3535
"Invalid src method name in ./src/Counter.sol on line 23: internalShouldHaveLeadingUnderscore",
3636
"Invalid src method name in ./src/Counter.sol on line 25: privateShouldHaveLeadingUnderscore",
37+
"Invalid src method name in ./src/CounterIgnored4.sol on line 29: missingLeadingUnderscoreAndNotIgnored",
3738
"Invalid test name in ./test/Counter.t.sol on line 16: testIncrementBadName",
3839
"Invalid directive in ./src/Counter.sol: Invalid inline config item: this directive is invalid",
3940
"error: Convention checks failed, see details above",

0 commit comments

Comments
 (0)