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
146 changes: 124 additions & 22 deletions crates/pyrefly_python/src/ignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,19 +459,56 @@ fn is_in_multiline_string(
}
}

/// A whole-file ignore directive parsed from the file's preamble.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IgnoreAll {
/// The line where the directive for this tool first appears.
pub line: LineNumber,
/// The error kinds the directive suppresses. An empty list means *all*
/// errors. Non-empty lists only ever occur for `Tool::Pyrefly` (via
/// `# pyrefly: ignore-errors[kind, ...]`); the parser never records kinds
/// for other tools, so consumers can treat an empty list as "suppress all".
pub kinds: Vec<String>,
}

/// Parse top-level `ignore-errors` / `ignore-all-errors` / `type: ignore` directives.
///
/// Scans the beginning of the file for comment-only lines (including blank lines
/// and lines inside multiline strings like docstrings). Returns the map of tools
/// with ignore-all directives and the line number where each directive appears.
/// with ignore-all directives, where each directive records its line and the
/// error kinds it suppresses (empty = all errors).
///
/// `# pyrefly: ignore-errors[kind, ...]` suppresses only the listed kinds for the
/// whole file; multiple such lines accumulate. A bare `# pyrefly: ignore-errors`
/// (or any other tool's directive) suppresses everything and overrides any kinds.
///
/// After a docstring, only `ignore-errors` directives are recognized — bare
/// `# type: ignore` is not, since it could plausibly be meant as a per-line
/// suppression for code that follows.
pub fn parse_ignore_all(
code: &str,
multiline_string_ranges: &[(LineNumber, LineNumber)],
) -> SmallMap<Tool, LineNumber> {
) -> SmallMap<Tool, IgnoreAll> {
// Record an ignore-all directive, merging with any earlier one for the same
// tool: empty `kinds` (suppress all) wins over specific kinds, and multiple
// specific-kind directives union together. The earliest line is kept.
fn record(
res: &mut SmallMap<Tool, IgnoreAll>,
tool: Tool,
line: LineNumber,
kinds: Vec<String>,
) {
if let Some(existing) = res.get_mut(&tool) {
if kinds.is_empty() {
existing.kinds.clear();
} else if !existing.kinds.is_empty() {
existing.kinds.extend(kinds);
}
} else {
res.insert(tool, IgnoreAll { line, kinds });
}
}

let mut res = SmallMap::new();
let mut prev_ignore = None;
let mut seen_docstring = false;
Expand Down Expand Up @@ -504,7 +541,7 @@ 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);
record(&mut res, tool, prev_line, Vec::new());
prev_ignore = None;
}

Expand All @@ -514,11 +551,25 @@ pub fn parse_ignore_all(
}
lex.trim_start();
if lex.starts_with("pyre-ignore-all-errors") {
res.entry(Tool::Pyre).or_insert(line);
record(&mut res, Tool::Pyre, line, Vec::new());
} 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") {
lex.trim_start();
if lex.blank() {
// Bare `ignore-errors`: suppress every error for this tool.
record(&mut res, tool, line, Vec::new());
} else if tool == Tool::Pyrefly
&& lex.starts_with("[")
&& let Some((inside, after)) = lex.rest().split_once(']')
&& after.trim().is_empty()
{
// `# pyrefly: ignore-errors[kind, ...]`: suppress only the
// listed kinds. Other tools don't support kind lists, so the
// directive is ignored for them (matching prior behavior).
let kinds = inside.split(',').map(|x| x.trim().to_owned()).collect();
record(&mut res, tool, line, kinds);
}
} 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 Down Expand Up @@ -700,14 +751,12 @@ x = """
#[test]
fn test_parse_ignore_all() {
fn f(x: &str, ignores: &[(Tool, u32)]) {
assert_eq!(
parse_ignore_all(x, &[]),
ignores
.iter()
.map(|x| (x.0, LineNumber::new(x.1).unwrap()))
.collect(),
"{x:?}"
);
let got: Vec<(Tool, u32)> = parse_ignore_all(x, &[])
.into_iter()
.map(|(tool, ig)| (tool, ig.line.get()))
.collect();
let want: Vec<(Tool, u32)> = ignores.to_vec();
assert_eq!(got, want, "{x:?}");
}

f("# pyrefly: ignore-errors\nx = 5", &[(Tool::Pyrefly, 1)]);
Expand Down Expand Up @@ -738,17 +787,70 @@ x = """
f("# pyrefly: ignore-errors \nx = 5", &[(Tool::Pyrefly, 1)]);
}

#[test]
fn test_parse_ignore_all_with_codes() {
fn f(x: &str, ignores: &[(Tool, u32, &[&str])]) {
let got: Vec<(Tool, u32, Vec<String>)> = parse_ignore_all(x, &[])
.into_iter()
.map(|(tool, ig)| (tool, ig.line.get(), ig.kinds))
.collect();
let want: Vec<(Tool, u32, Vec<String>)> = ignores
.iter()
.map(|(tool, line, kinds)| {
(
*tool,
*line,
kinds.iter().map(|k| (*k).to_owned()).collect(),
)
})
.collect();
assert_eq!(got, want, "{x:?}");
}

// A single kind suppresses just that kind for the whole file.
f(
"# pyrefly: ignore-errors[bad-return]\nx = 5",
&[(Tool::Pyrefly, 1, &["bad-return"])],
);
// Comma-separated kinds on one line.
f(
"# pyrefly: ignore-errors[implicit-any-parameter, implicit-any-type-argument]\nx = 5",
&[(
Tool::Pyrefly,
1,
&["implicit-any-parameter", "implicit-any-type-argument"],
)],
);
// One kind per line accumulates into a single directive.
f(
"# pyrefly: ignore-errors[a]\n# pyrefly: ignore-errors[b]\nx = 5",
&[(Tool::Pyrefly, 1, &["a", "b"])],
);
// A bare `ignore-errors` overrides kinds: suppress everything.
f(
"# pyrefly: ignore-errors[a]\n# pyrefly: ignore-errors\nx = 5",
&[(Tool::Pyrefly, 1, &[])],
);
f(
"# pyrefly: ignore-errors\n# pyrefly: ignore-errors[a]\nx = 5",
&[(Tool::Pyrefly, 1, &[])],
);
// Only pyrefly honors kind lists; for other tools the bracketed form is
// not recognized as a directive at all.
f("# mypy: ignore-errors[a]\nx = 5", &[]);
// Trailing junk after the bracket invalidates the directive.
f("# pyrefly: ignore-errors[a] because\nx = 5", &[]);
}

#[test]
fn test_parse_ignore_all_with_docstring() {
fn f(x: &str, ranges: &[(LineNumber, LineNumber)], ignores: &[(Tool, u32)]) {
assert_eq!(
parse_ignore_all(x, ranges),
ignores
.iter()
.map(|x| (x.0, LineNumber::new(x.1).unwrap()))
.collect(),
"{x:?}"
);
let got: Vec<(Tool, u32)> = parse_ignore_all(x, ranges)
.into_iter()
.map(|(tool, ig)| (tool, ig.line.get()))
.collect();
let want: Vec<(Tool, u32)> = ignores.to_vec();
assert_eq!(got, want, "{x:?}");
}

// ignore-errors after a docstring should work
Expand Down
20 changes: 14 additions & 6 deletions pyrefly/lib/error/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::mem;

use dupe::Dupe;
use pyrefly_config::error_kind::ErrorKind;
use pyrefly_python::ignore::IgnoreAll;
use pyrefly_python::ignore::Tool;
use pyrefly_util::lined_buffer::LineNumber;
use pyrefly_util::lock::Mutex;
Expand Down Expand Up @@ -219,16 +220,23 @@ impl ErrorCollector {
fn is_error_suppressed(
err: &Error,
fstring_ranges: &[(LineNumber, LineNumber)],
ignore_all: &SmallMap<Tool, LineNumber>,
ignore_all: &SmallMap<Tool, IgnoreAll>,
error_config: &ErrorConfig,
) -> bool {
// Check whole-file ignore-all directives first.
// UnusedIgnore errors cannot be suppressed to prevent infinite loops.
// An `IgnoreAll` with empty `kinds` suppresses everything; otherwise it
// suppresses only the listed kinds (or any parent kind they map to).
if err.error_kind() != ErrorKind::UnusedIgnore
&& error_config
.enabled_ignores
.iter()
.any(|tool| ignore_all.contains_key(tool))
&& error_config.enabled_ignores.iter().any(|tool| {
ignore_all.get(tool).is_some_and(|ig| {
ig.kinds.is_empty()
|| err
.error_kind()
.suppression_names()
.any(|name| ig.kinds.iter().any(|k| k == name))
})
})
{
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: &SmallMap<Tool, IgnoreAll>,
result: &mut CollectedErrors,
) {
let mut errors = self.errors.lock();
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::IgnoreAll;
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: SmallMap<Tool, IgnoreAll>,
}

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

testcase!(
test_pyrefly_top_level_ignore_specific_kind,
r#"
# pyrefly: ignore-errors[bad-assignment]
x: int = "oops"
def f() -> int:
return "oops" # E: Returned type `Literal['oops']` is not assignable to declared return type `int`
"#,
);

testcase!(
test_pyrefly_top_level_ignore_specific_kinds_one_line,
r#"
# pyrefly: ignore-errors[bad-assignment, bad-return]
x: int = "oops"
def f() -> int:
return "oops"
"#,
);

testcase!(
test_pyrefly_top_level_ignore_specific_kinds_multiline,
r#"
# pyrefly: ignore-errors[bad-assignment]
# pyrefly: ignore-errors[bad-return]
x: int = "oops"
def f() -> int:
return "oops"
"#,
);

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

To suppress only specific error kinds for the whole file, list them in
brackets. Multiple kinds can be comma-separated on one line, or split across
several `ignore-errors` lines; the lists accumulate. This only applies to
`pyrefly: ignore-errors` (other tools support only the bare, suppress-all form).

```python
# pyrefly: ignore-errors[implicit-any-parameter, implicit-any-type-argument]
# pyrefly: ignore-errors[implicit-any-empty-container]
```

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

```
Expand Down
Loading