Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 81 additions & 22 deletions crates/pyrefly_python/src/ignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! Note that Pyright will only honor such codes after `# pyright: ignore[code]`.
//!
//! You can also use `# mypy: ignore-errors`, `# pyrefly: ignore-errors`
//! or `# pyrefly: ignore-errors[invalid-type]`
//! or `# type: ignore` at the beginning of a file to suppress all errors.
//!
//! For Pyre compatibility we also allow `# pyre-ignore` and `# pyre-fixme`
Expand Down Expand Up @@ -471,8 +472,8 @@ fn is_in_multiline_string(
pub fn parse_ignore_all(
code: &str,
multiline_string_ranges: &[(LineNumber, LineNumber)],
) -> SmallMap<Tool, LineNumber> {
let mut res = SmallMap::new();
) -> Vec<Suppression> {
let mut res = Vec::new();
let mut prev_ignore = None;
let mut seen_docstring = false;

Expand Down Expand Up @@ -504,7 +505,11 @@ pub fn parse_ignore_all(
if let Some((tool, prev_line)) = prev_ignore {
// The previous `# type: ignore` was followed by another comment or
// blank line, so it is a whole-file suppression.
res.entry(tool).or_insert(prev_line);
res.push(Suppression {
tool,
kind: Vec::new(),
comment_line: prev_line,
});
prev_ignore = None;
}

Expand All @@ -514,11 +519,23 @@ pub fn parse_ignore_all(
}
lex.trim_start();
if lex.starts_with("pyre-ignore-all-errors") {
res.entry(Tool::Pyre).or_insert(line);
res.push(Suppression {
tool: Tool::Pyre,
kind: Vec::new(),
comment_line: line,
});
} else if let Some(tool) = lex.starts_with_tool() {
lex.trim_start();
if lex.starts_with("ignore-errors") && lex.blank() {
res.entry(tool).or_insert(line);
if lex.starts_with("ignore-errors") {
if let Some(kind) = parse_ignore_all_kind(&mut lex) {
if tool == Tool::Pyrefly || kind.is_empty() {
res.push(Suppression {
tool,
kind,
comment_line: line,
});
}
}
} else if !seen_docstring && lex.starts_with("ignore") && lex.blank() {
// After a docstring, bare `# type: ignore` is not recognized
// as an ignore-all directive.
Expand All @@ -529,6 +546,21 @@ pub fn parse_ignore_all(
res
}

fn parse_ignore_all_kind(lex: &mut Lexer<'_>) -> Option<Vec<String>> {
lex.trim_start();
if lex.starts_with("[") {
let rest = lex.0;
let (inside, after) = rest.split_once(']').unwrap_or((rest, ""));
let mut after = Lexer(after);
if after.blank() {
return Some(inside.split(',').map(|x| x.trim().to_owned()).collect());
}
} else if lex.blank() {
return Some(Vec::new());
}
None
}

#[cfg(test)]
mod tests {
use pyrefly_util::prelude::SliceExt;
Expand Down Expand Up @@ -699,54 +731,81 @@ x = """

#[test]
fn test_parse_ignore_all() {
fn f(x: &str, ignores: &[(Tool, u32)]) {
fn f(x: &str, ignores: &[(Tool, u32, &[&str])]) {
assert_eq!(
parse_ignore_all(x, &[]),
ignores
.iter()
.map(|x| (x.0, LineNumber::new(x.1).unwrap()))
.collect(),
.map(|x| Suppression {
tool: x.0,
kind: x.2.iter().map(|x| (*x).to_owned()).collect(),
comment_line: LineNumber::new(x.1).unwrap(),
})
.collect::<Vec<_>>(),
"{x:?}"
);
}

f("# pyrefly: ignore-errors\nx = 5", &[(Tool::Pyrefly, 1)]);
f(
"# pyrefly: ignore-errors\nx = 5",
&[(Tool::Pyrefly, 1, &[])],
);
f(
"# pyrefly: ignore-errors[bad-assignment]\nx = 5",
&[(Tool::Pyrefly, 1, &["bad-assignment"])],
);
f(
"# pyrefly: ignore-errors [ bad-assignment, bad-return ]\nx = 5",
&[(Tool::Pyrefly, 1, &["bad-assignment", "bad-return"])],
);
f(
"# comment\n# pyrefly: ignore-errors\nx = 5",
&[(Tool::Pyrefly, 2)],
&[(Tool::Pyrefly, 2, &[])],
);
f(
"#comment\n # indent\n# pyrefly: ignore-errors\nx = 5",
&[(Tool::Pyrefly, 3)],
&[(Tool::Pyrefly, 3, &[])],
);
f("x = 5\n# pyrefly: ignore-errors", &[]);
f("# type: ignore\n\nx = 5", &[(Tool::Type, 1)]);
f("# type: ignore\n\nx = 5", &[(Tool::Type, 1, &[])]);
f(
"# comment\n# type: ignore\n# comment\nx = 5",
&[(Tool::Type, 2)],
&[(Tool::Type, 2, &[])],
);
f("# type: ignore\nx = 5", &[]);
f("# pyre-ignore-all-errors\nx = 5", &[(Tool::Pyre, 1)]);
f("# pyre-ignore-all-errors\nx = 5", &[(Tool::Pyre, 1, &[])]);
f(
"# mypy: ignore-errors\n#pyrefly:ignore-errors",
&[(Tool::Mypy, 1), (Tool::Pyrefly, 2)],
&[(Tool::Mypy, 1, &[]), (Tool::Pyrefly, 2, &[])],
);
f("# mypy: ignore-errors[bad-assignment]\nx = 5", &[]);

// Anything else on the line (other than space) makes it invalid
f("# pyrefly: ignore-errors because I want to\nx = 5", &[]);
f("# pyrefly: ignore-errors # because I want to\nx = 5", &[]);
f("# pyrefly: ignore-errors \nx = 5", &[(Tool::Pyrefly, 1)]);
f(
"# pyrefly: ignore-errors[bad-assignment] # because I want to\nx = 5",
&[],
);
f(
"# pyrefly: ignore-errors \nx = 5",
&[(Tool::Pyrefly, 1, &[])],
);
}

#[test]
fn test_parse_ignore_all_with_docstring() {
fn f(x: &str, ranges: &[(LineNumber, LineNumber)], ignores: &[(Tool, u32)]) {
fn f(x: &str, ranges: &[(LineNumber, LineNumber)], ignores: &[(Tool, u32, &[&str])]) {
assert_eq!(
parse_ignore_all(x, ranges),
ignores
.iter()
.map(|x| (x.0, LineNumber::new(x.1).unwrap()))
.collect(),
.map(|x| Suppression {
tool: x.0,
kind: x.2.iter().map(|x| (*x).to_owned()).collect(),
comment_line: LineNumber::new(x.1).unwrap(),
})
.collect::<Vec<_>>(),
"{x:?}"
);
}
Expand All @@ -758,7 +817,7 @@ x = """
LineNumber::from_zero_indexed(0),
LineNumber::from_zero_indexed(2),
)],
&[(Tool::Pyrefly, 4)],
&[(Tool::Pyrefly, 4, &[])],
);

// bare `# type: ignore` after docstring should NOT be recognized
Expand All @@ -778,7 +837,7 @@ x = """
LineNumber::from_zero_indexed(1),
LineNumber::from_zero_indexed(3),
)],
&[(Tool::Pyrefly, 1)],
&[(Tool::Pyrefly, 1, &[])],
);
}
}
2 changes: 1 addition & 1 deletion pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ impl Bindings {
metadata: Arc::new(BindingsMetadata::new()),
module_ranges: Arc::new(ModuleRanges {
multi_line: Vec::new(),
ignore_all: SmallMap::new(),
ignore_all: Vec::new(),
}),
scope_trace: None,
module_deletes: SmallSet::new(),
Expand Down
24 changes: 16 additions & 8 deletions pyrefly/lib/error/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use std::mem;

use dupe::Dupe;
use pyrefly_config::error_kind::ErrorKind;
use pyrefly_python::ignore::Suppression;
use pyrefly_python::ignore::Tool;
use pyrefly_util::lined_buffer::LineNumber;
use pyrefly_util::lock::Mutex;
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
use starlark_map::small_map::SmallMap;

use crate::config::error::ErrorConfig;
use crate::config::error_kind::Severity;
Expand Down Expand Up @@ -219,16 +219,24 @@ impl ErrorCollector {
fn is_error_suppressed(
err: &Error,
fstring_ranges: &[(LineNumber, LineNumber)],
ignore_all: &SmallMap<Tool, LineNumber>,
ignore_all: &[Suppression],
error_config: &ErrorConfig,
) -> bool {
// Check whole-file ignore-all directives first.
// UnusedIgnore errors cannot be suppressed to prevent infinite loops.
if err.error_kind() != ErrorKind::UnusedIgnore
&& error_config
.enabled_ignores
.iter()
.any(|tool| ignore_all.contains_key(tool))
&& err.error_kind().suppression_names().any(|kind| {
ignore_all.iter().any(|supp| {
error_config.enabled_ignores.contains(&supp.tool())
&& match supp.tool() {
Tool::Pyrefly => {
supp.error_codes().is_empty()
|| supp.error_codes().iter().any(|x| x == kind)
}
_ => true,
}
})
})
{
return true;
}
Expand Down Expand Up @@ -258,7 +266,7 @@ impl ErrorCollector {
&self,
error_config: &ErrorConfig,
fstring_ranges: &[(LineNumber, LineNumber)],
ignore_all: &SmallMap<Tool, LineNumber>,
ignore_all: &[Suppression],
result: &mut CollectedErrors,
) {
let mut errors = self.errors.lock();
Expand Down Expand Up @@ -290,7 +298,7 @@ impl ErrorCollector {

pub fn collect(&self, error_config: &ErrorConfig) -> CollectedErrors {
let mut result = CollectedErrors::default();
self.collect_into(error_config, &[], &SmallMap::new(), &mut result);
self.collect_into(error_config, &[], &[], &mut result);
result
}
}
Expand Down
3 changes: 2 additions & 1 deletion pyrefly/lib/state/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use dupe::Dupe;
use pyrefly_config::error_kind::ErrorKind;
use pyrefly_config::error_kind::Severity;
use pyrefly_python::ignore::Ignore;
use pyrefly_python::ignore::Suppression;
use pyrefly_python::ignore::Tool;
use pyrefly_python::ignore::find_comment_start_in_line;
use pyrefly_python::ignore::parse_ignore_all;
Expand Down Expand Up @@ -233,7 +234,7 @@ pub struct ModuleRanges {
/// Multi-line string and backslash-continuation ranges.
pub multi_line: Vec<(LineNumber, LineNumber)>,
/// Top-level ignore-all directives (e.g. `# pyrefly: ignore-errors`).
pub ignore_all: SmallMap<Tool, LineNumber>,
pub ignore_all: Vec<Suppression>,
}

impl ModuleRanges {
Expand Down
9 changes: 9 additions & 0 deletions pyrefly/lib/test/suppression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ testcase!(
"#,
);

testcase!(
test_pyrefly_top_level_ignore_typed,
r#"
# pyrefly: ignore-errors[bad-assignment]
x: int = "x"
3 + "3" # E:
"#,
);

testcase!(
test_pyrefly_top_level_ignore_wrong_same_line,
r#"
Expand Down
6 changes: 6 additions & 0 deletions website/docs/error-suppressions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def bar() -> int:
/>
</pre>

To ignore one error type across a file, add the error code to the file-level comment:

```python
# pyrefly: ignore-errors[bad-assignment]
```

Pyrefly can automatically suppress all type errors in your project by running:

```
Expand Down
Loading