diff --git a/crates/pyrefly_python/src/ignore.rs b/crates/pyrefly_python/src/ignore.rs index 25b5683db6..c2df81b013 100644 --- a/crates/pyrefly_python/src/ignore.rs +++ b/crates/pyrefly_python/src/ignore.rs @@ -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` @@ -471,8 +472,8 @@ fn is_in_multiline_string( pub fn parse_ignore_all( code: &str, multiline_string_ranges: &[(LineNumber, LineNumber)], -) -> SmallMap { - let mut res = SmallMap::new(); +) -> Vec { + let mut res = Vec::new(); let mut prev_ignore = None; let mut seen_docstring = false; @@ -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; } @@ -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. @@ -529,6 +546,21 @@ pub fn parse_ignore_all( res } +fn parse_ignore_all_kind(lex: &mut Lexer<'_>) -> Option> { + 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; @@ -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::>(), "{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::>(), "{x:?}" ); } @@ -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 @@ -778,7 +837,7 @@ x = """ LineNumber::from_zero_indexed(1), LineNumber::from_zero_indexed(3), )], - &[(Tool::Pyrefly, 1)], + &[(Tool::Pyrefly, 1, &[])], ); } } diff --git a/pyrefly/lib/binding/bindings.rs b/pyrefly/lib/binding/bindings.rs index 6300c8eeb1..c9d6cb8ccc 100644 --- a/pyrefly/lib/binding/bindings.rs +++ b/pyrefly/lib/binding/bindings.rs @@ -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(), diff --git a/pyrefly/lib/error/collector.rs b/pyrefly/lib/error/collector.rs index 6f6c2375ca..63346eafb1 100644 --- a/pyrefly/lib/error/collector.rs +++ b/pyrefly/lib/error/collector.rs @@ -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; @@ -219,16 +219,24 @@ impl ErrorCollector { fn is_error_suppressed( err: &Error, fstring_ranges: &[(LineNumber, LineNumber)], - ignore_all: &SmallMap, + 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; } @@ -258,7 +266,7 @@ impl ErrorCollector { &self, error_config: &ErrorConfig, fstring_ranges: &[(LineNumber, LineNumber)], - ignore_all: &SmallMap, + ignore_all: &[Suppression], result: &mut CollectedErrors, ) { let mut errors = self.errors.lock(); @@ -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 } } diff --git a/pyrefly/lib/state/errors.rs b/pyrefly/lib/state/errors.rs index 5659d2cec7..8ce0d90cda 100644 --- a/pyrefly/lib/state/errors.rs +++ b/pyrefly/lib/state/errors.rs @@ -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; @@ -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, + pub ignore_all: Vec, } impl ModuleRanges { diff --git a/pyrefly/lib/test/suppression.rs b/pyrefly/lib/test/suppression.rs index 6dcea4ef4b..e0f678705d 100644 --- a/pyrefly/lib/test/suppression.rs +++ b/pyrefly/lib/test/suppression.rs @@ -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#" diff --git a/website/docs/error-suppressions.mdx b/website/docs/error-suppressions.mdx index 9cb459c2d5..f69600b9c7 100644 --- a/website/docs/error-suppressions.mdx +++ b/website/docs/error-suppressions.mdx @@ -68,6 +68,12 @@ def bar() -> int: /> +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: ```