diff --git a/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_0.py b/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_0.py index 5c8402c08c1253..b0fcaac2cc7466 100644 --- a/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_0.py +++ b/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_0.py @@ -1,11 +1,16 @@ x = 1 # type: ignore -x = 1 # type ignore x = 1 # type:ignore +x = 1 # type: ignore[attr-defined] # type: ignore x = 1 +x = 1 # type ignore x = 1 # type ignore # noqa x = 1 # type: ignore[attr-defined] x = 1 # type: ignore[attr-defined, name-defined] +x = 1 # type: ignore[attr-defined] # type: ignore[type-mismatch] x = 1 # type: ignore[type-mismatch] # noqa +x = 1 # type: ignore [attr-defined] +x = 1 # type: ignore [attr-defined, name-defined] +x = 1 # type: ignore [type-mismatch] # noqa x = 1 # type: Union[int, str] x = 1 # type: ignoreme diff --git a/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_1.py b/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_1.py new file mode 100644 index 00000000000000..d65adc03c6b937 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_1.py @@ -0,0 +1,16 @@ +x = 1 # pyright: ignore +x = 1 # pyright:ignore +x = 1 # pyright: ignore[attr-defined] # pyright: ignore + +x = 1 +x = 1 # pyright ignore +x = 1 # pyright ignore # noqa +x = 1 # pyright: ignore[attr-defined] +x = 1 # pyright: ignore[attr-defined, name-defined] +x = 1 # pyright: ignore[attr-defined] # pyright: ignore[type-mismatch] +x = 1 # pyright: ignore[type-mismatch] # noqa +x = 1 # pyright: ignore [attr-defined] +x = 1 # pyright: ignore [attr-defined, name-defined] +x = 1 # pyright: ignore [type-mismatch] # noqa +x = 1 # pyright: Union[int, str] +x = 1 # pyright: ignoreme diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 7cbbd9b46c77f8..a754f1b36773ae 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -76,9 +76,7 @@ pub fn check_physical_lines( } if enforce_blanket_type_ignore { - if let Some(diagnostic) = blanket_type_ignore(index, line) { - diagnostics.push(diagnostic); - } + blanket_type_ignore(&mut diagnostics, index, line); } if enforce_blanket_noqa { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index b53eae44c0da8f..48426acea3dbed 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -35,11 +35,6 @@ impl Violation for TypeCommentInStub { } } -static TYPE_COMMENT_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^#\s*type:\s*([^#]+)(\s*#.*?)?$").unwrap()); -static TYPE_IGNORE_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^#\s*type:\s*ignore([^#]+)?(\s*#.*?)?$").unwrap()); - /// PYI033 pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec { let mut diagnostics = vec![]; @@ -60,3 +55,9 @@ pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec { diagnostics } + +static TYPE_COMMENT_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^#\s*(type|pyright):\s*([^#]+)(\s*#.*?)?$").unwrap()); + +static TYPE_IGNORE_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^#\s*(type|pyright):\s*ignore([^#]+)?(\s*#.*?)?$").unwrap()); diff --git a/crates/ruff/src/rules/pygrep_hooks/mod.rs b/crates/ruff/src/rules/pygrep_hooks/mod.rs index ffc3e71e07c33d..b31c5028717ca9 100644 --- a/crates/ruff/src/rules/pygrep_hooks/mod.rs +++ b/crates/ruff/src/rules/pygrep_hooks/mod.rs @@ -18,6 +18,7 @@ mod tests { #[test_case(Rule::DeprecatedLogWarn, Path::new("PGH002_0.py"); "PGH002_0")] #[test_case(Rule::DeprecatedLogWarn, Path::new("PGH002_1.py"); "PGH002_1")] #[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_0.py"); "PGH003_0")] + #[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_1.py"); "PGH003_1")] #[test_case(Rule::BlanketNOQA, Path::new("PGH004_0.py"); "PGH004_0")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs index c5a8a5ffbd0a9f..022ae0428299f7 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use once_cell::sync::Lazy; use regex::Regex; use rustpython_parser::ast::Location; @@ -16,18 +17,85 @@ impl Violation for BlanketTypeIgnore { } } -static BLANKET_TYPE_IGNORE_REGEX: Lazy = - Lazy::new(|| Regex::new(r"# type:? *ignore($|\s)").unwrap()); - -/// PGH003 - use of blanket type ignore comments -pub fn blanket_type_ignore(lineno: usize, line: &str) -> Option { - BLANKET_TYPE_IGNORE_REGEX.find(line).map(|m| { - Diagnostic::new( - BlanketTypeIgnore, - Range::new( - Location::new(lineno + 1, m.start()), - Location::new(lineno + 1, m.end()), - ), - ) - }) +/// PGH003 +pub fn blanket_type_ignore(diagnostics: &mut Vec, lineno: usize, line: &str) { + for match_ in TYPE_IGNORE_PATTERN.find_iter(line) { + if let Ok(codes) = parse_type_ignore_tag(line[match_.end()..].trim()) { + if codes.is_empty() { + let start = line[..match_.start()].chars().count(); + let end = start + line[match_.start()..match_.end()].chars().count(); + diagnostics.push(Diagnostic::new( + BlanketTypeIgnore, + Range::new( + Location::new(lineno + 1, start), + Location::new(lineno + 1, end), + ), + )); + } + } + } +} + +// Match, e.g., `# type: ignore` or `# type: ignore[attr-defined]`. +// See: https://github.com/python/mypy/blob/b43e0d34247a6d1b3b9d9094d184bbfcb9808bb9/mypy/fastparse.py#L248 +static TYPE_IGNORE_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"#\s*(type|pyright):\s*ignore\s*").unwrap()); + +// Match, e.g., `[attr-defined]` or `[attr-defined, misc]`. +// See: https://github.com/python/mypy/blob/b43e0d34247a6d1b3b9d9094d184bbfcb9808bb9/mypy/fastparse.py#L327 +static TYPE_IGNORE_TAG_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^\s*\[(?P[^]#]*)]\s*(#.*)?$").unwrap()); + +/// Parse the optional `[...]` tag in a `# type: ignore[...]` comment. +/// +/// Returns a list of error codes to ignore, or an empty list if the tag is +/// a blanket ignore. +fn parse_type_ignore_tag(tag: &str) -> Result> { + // See: https://github.com/python/mypy/blob/b43e0d34247a6d1b3b9d9094d184bbfcb9808bb9/mypy/fastparse.py#L316 + // No tag -- ignore all errors. + let trimmed = tag.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + return Ok(vec![]); + } + + // Parse comma-separated list of error codes. + TYPE_IGNORE_TAG_PATTERN + .captures(tag) + .map(|captures| { + captures + .name("codes") + .unwrap() + .as_str() + .split(',') + .map(str::trim) + .collect() + }) + .ok_or_else(|| anyhow!("Invalid type ignore tag: {tag}")) +} + +#[cfg(test)] +mod tests { + + #[test] + fn type_ignore_tag() { + let tag = ""; + let result = super::parse_type_ignore_tag(tag); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Vec::<&str>::new()); + + let tag = "[attr-defined]"; + let result = super::parse_type_ignore_tag(tag); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["attr-defined"]); + + let tag = " [attr-defined]"; + let result = super::parse_type_ignore_tag(tag); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["attr-defined"]); + + let tag = "[attr-defined, misc]"; + let result = super::parse_type_ignore_tag(tag); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["attr-defined", "misc"]); + } } diff --git a/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_0.py.snap b/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_0.py.snap index 12f724586a204c..300376d6a7dcda 100644 --- a/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_0.py.snap +++ b/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_0.py.snap @@ -5,24 +5,24 @@ PGH003_0.py:1:8: PGH003 Use specific rule codes when ignoring type issues | 1 | x = 1 # type: ignore | ^^^^^^^^^^^^^^ PGH003 -2 | x = 1 # type ignore -3 | x = 1 # type:ignore +2 | x = 1 # type:ignore +3 | x = 1 # type: ignore[attr-defined] # type: ignore | PGH003_0.py:2:8: PGH003 Use specific rule codes when ignoring type issues | 2 | x = 1 # type: ignore -3 | x = 1 # type ignore +3 | x = 1 # type:ignore | ^^^^^^^^^^^^^ PGH003 -4 | x = 1 # type:ignore +4 | x = 1 # type: ignore[attr-defined] # type: ignore | -PGH003_0.py:3:8: PGH003 Use specific rule codes when ignoring type issues +PGH003_0.py:3:38: PGH003 Use specific rule codes when ignoring type issues | 3 | x = 1 # type: ignore -4 | x = 1 # type ignore -5 | x = 1 # type:ignore - | ^^^^^^^^^^^^^ PGH003 +4 | x = 1 # type:ignore +5 | x = 1 # type: ignore[attr-defined] # type: ignore + | ^^^^^^^^^^^^^^ PGH003 6 | 7 | x = 1 | diff --git a/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_1.py.snap b/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_1.py.snap new file mode 100644 index 00000000000000..46da760b4bf001 --- /dev/null +++ b/crates/ruff/src/rules/pygrep_hooks/snapshots/ruff__rules__pygrep_hooks__tests__PGH003_PGH003_1.py.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff/src/rules/pygrep_hooks/mod.rs +--- +PGH003_1.py:1:8: PGH003 Use specific rule codes when ignoring type issues + | +1 | x = 1 # pyright: ignore + | ^^^^^^^^^^^^^^^^^ PGH003 +2 | x = 1 # pyright:ignore +3 | x = 1 # pyright: ignore[attr-defined] # pyright: ignore + | + +PGH003_1.py:2:8: PGH003 Use specific rule codes when ignoring type issues + | +2 | x = 1 # pyright: ignore +3 | x = 1 # pyright:ignore + | ^^^^^^^^^^^^^^^^ PGH003 +4 | x = 1 # pyright: ignore[attr-defined] # pyright: ignore + | + +PGH003_1.py:3:41: PGH003 Use specific rule codes when ignoring type issues + | +3 | x = 1 # pyright: ignore +4 | x = 1 # pyright:ignore +5 | x = 1 # pyright: ignore[attr-defined] # pyright: ignore + | ^^^^^^^^^^^^^^^^^ PGH003 +6 | +7 | x = 1 + | + +