diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 4c4e06137eef8..2c87d4cd3e1e8 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -159,6 +159,42 @@ def _(x: int): assert_type(x, int) ``` +## `blanket-ignore-comment` + + +Default level: ignore · +Added in 0.0.57 · +Related issues · +View source + + + +**What it does** + + +Checks for `ty: ignore` comments that don't specify which rules to ignore. + +**Why is this bad?** + + +A blanket `ty: ignore` comment suppresses every type-checking diagnostic on the +applicable line or file. Specifying rule codes documents which diagnostics are +expected and prevents the comment from silencing unrelated errors. + +**Examples** + + +```py +# error +value = unknown # ty: ignore +``` + +Use instead: + +```py +value = unknown # ty: ignore[unresolved-reference] +``` + ## `call-abstract-method` diff --git a/crates/ty_python_semantic/resources/lint_docs/blanket-ignore-comment.md b/crates/ty_python_semantic/resources/lint_docs/blanket-ignore-comment.md new file mode 100644 index 0000000000000..d0b846d2f549a --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/blanket-ignore-comment.md @@ -0,0 +1,22 @@ +## What it does + +Checks for `ty: ignore` comments that don't specify which rules to ignore. + +## Why is this bad? + +A blanket `ty: ignore` comment suppresses every type-checking diagnostic on the +applicable line or file. Specifying rule codes documents which diagnostics are +expected and prevents the comment from silencing unrelated errors. + +## Examples + +```py +# error +value = unknown # ty: ignore +``` + +Use instead: + +```py +value = unknown # ty: ignore[unresolved-reference] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/public_types.md b/crates/ty_python_semantic/resources/mdtest/public_types.md index 186ba06e8eb73..cc5d73313a45d 100644 --- a/crates/ty_python_semantic/resources/mdtest/public_types.md +++ b/crates/ty_python_semantic/resources/mdtest/public_types.md @@ -299,7 +299,7 @@ this: ```py try: - import optional_dependency # ty: ignore + import optional_dependency # ty: ignore[unresolved-import] except ImportError: optional_dependency = None diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/blanket_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/blanket_ignore.md new file mode 100644 index 0000000000000..ea63b25c00259 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/blanket_ignore.md @@ -0,0 +1,72 @@ +# Blanket `ty: ignore` comments + +The optional `blanket-ignore-comment` rule requires `ty: ignore` comments to include specific rule +codes. + +```toml +[rules] +blanket-ignore-comment = "error" +``` + +## Line-level ignores + +```py +# error: [blanket-ignore-comment] +a = unresolved # ty: ignore + +b = unresolved # ty: ignore[unresolved-reference] + +# `blanket-ignore-comment` covers only `ty: ignore`; we can also add `blanket-type-ignore-comment`, +# parallel to `unused-ignore-comment` and `unused-type-ignore-comment`. In the meantime, blanket +# `type: ignore` can also be caught by Ruff's PGH003 rule. +c = unresolved # type: ignore +``` + +## Unused ignore comments + +When `unused-ignore-comment` is enabled, an unused `ty: ignore` comment without rule codes triggers +both rules. + +```py +# error: [unused-ignore-comment] "Unused `ty: ignore` without a code" +d = 1 # ty: ignore[] + +# error: [blanket-ignore-comment] +# error: [unused-ignore-comment] "Unused blanket `ty: ignore` directive" +e = 1 # ty: ignore +``` + +## Suppression diagnostics + +Suppression-related diagnostics are checked before `blanket-ignore-comment`. A blanket ignore that +suppresses an `ignore-comment-unknown-rule` or `invalid-ignore-comment` diagnostic therefore counts +as used: + +```py +# The nested ignore contains an unknown rule. +# error: [blanket-ignore-comment] +a = 1 # ty: ignore # ty: ignore[not-a-rule] + +# The nested ignore is invalid. +# error: [blanket-ignore-comment] +b = 1 # ty: ignore # ty: ignore[*] +``` + +## File-level ignores + +The rule also detects file-level blanket ignores: + +```py +# error: [blanket-ignore-comment] +# ty: ignore + +a = unresolved +``` + +## Suppressing the rule + +A blanket ignore can be suppressed by a code-specific ignore: + +```py +a = unresolved # ty: ignore # ty: ignore[blanket-ignore-comment] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md index 488c52399ddd6..e7971313b81b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -1,6 +1,13 @@ # Suppressing errors with `ty: ignore` -Type check errors can be suppressed by a `ty: ignore` comment on the same line as the violation. +Type check errors can be suppressed by a `ty: ignore` comment on the same line as the violation. The +optional `blanket-ignore-comment` rule is tested separately so that these examples can exercise bare +ignore comments. + +```toml +[rules] +blanket-ignore-comment = "ignore" +``` ## Simple `ty: ignore` diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 1a7b6262e049e..098bfa071a24f 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -4,7 +4,8 @@ )] use crate::lint::{LintRegistry, LintRegistryBuilder}; use crate::suppression::{ - IGNORE_COMMENT_UNKNOWN_RULE, INVALID_IGNORE_COMMENT, UNUSED_TYPE_IGNORE_COMMENT, + BLANKET_IGNORE_COMMENT, IGNORE_COMMENT_UNKNOWN_RULE, INVALID_IGNORE_COMMENT, + UNUSED_TYPE_IGNORE_COMMENT, }; use crate::types::check_types; pub use db::Db; @@ -85,6 +86,7 @@ pub fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&UNUSED_TYPE_IGNORE_COMMENT); registry.register_lint(&IGNORE_COMMENT_UNKNOWN_RULE); registry.register_lint(&INVALID_IGNORE_COMMENT); + registry.register_lint(&BLANKET_IGNORE_COMMENT); } #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index 79b1534da9fbb..2ac717b7376ad 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -58,6 +58,15 @@ declare_lint! { } } +declare_lint! { + #[doc = include_str!("../resources/lint_docs/blanket-ignore-comment.md")] + pub(crate) static BLANKET_IGNORE_COMMENT = { + summary: "detects blanket `ty: ignore` comments", + status: LintStatus::stable("0.0.57"), + default_level: Level::Ignore, + } +} + pub fn is_unused_ignore_comment_lint(name: LintName) -> bool { name == UNUSED_IGNORE_COMMENT.name() || name == UNUSED_TYPE_IGNORE_COMMENT.name() } @@ -127,11 +136,39 @@ pub(crate) fn check_suppressions( check_unknown_rule(&mut context); check_invalid_suppression(&mut context); + check_blanket_suppressions(&mut context); check_unused_suppressions(&mut context); context.diagnostics.into_inner().into_diagnostics() } +fn check_blanket_suppressions(context: &mut CheckSuppressionsContext) { + if context.is_lint_disabled(&BLANKET_IGNORE_COMMENT) { + return; + } + + for suppression in context.suppressions.iter().filter(|suppression| { + suppression.kind == SuppressionKind::Ty && suppression.target == SuppressionTarget::All + }) { + // A blanket suppression cannot suppress its own diagnostic, but a code-specific + // suppression can. + if let Some(code_suppression) = context + .suppressions + .lint_suppressions(suppression.range, LintId::of(&BLANKET_IGNORE_COMMENT)) + .find(|candidate| candidate.target.is_lint()) + { + context + .diagnostics + .borrow_mut() + .mark_used(code_suppression.id()); + } else if let Some(diag) = + context.report_unchecked(&BLANKET_IGNORE_COMMENT, suppression.range) + { + diag.into_diagnostic("Use specific rule codes in `ty: ignore`"); + } + } +} + /// Checks for `ty: ignore` and `type: ignore[ty:]` comments that reference unknown rules. fn check_unknown_rule(context: &mut CheckSuppressionsContext) { if context.is_lint_disabled(&IGNORE_COMMENT_UNKNOWN_RULE) { diff --git a/ty.schema.json b/ty.schema.json index 1c2b10cf37b92..8bf98fb204631 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -360,6 +360,16 @@ } ] }, + "blanket-ignore-comment": { + "title": "detects blanket `ty: ignore` comments", + "description": "## What it does\n\nChecks for `ty: ignore` comments that don't specify which rules to ignore.\n\n## Why is this bad?\n\nA blanket `ty: ignore` comment suppresses every type-checking diagnostic on the\napplicable line or file. Specifying rule codes documents which diagnostics are\nexpected and prevents the comment from silencing unrelated errors.\n\n## Examples\n\n```py\n# error\nvalue = unknown # ty: ignore\n```\n\nUse instead:\n\n```py\nvalue = unknown # ty: ignore[unresolved-reference]\n```", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "call-abstract-method": { "title": "detects calls to abstract methods with trivial bodies on class objects", "description": "## What it does\n\nChecks for calls to abstract `@classmethod`s or `@staticmethod`s\nwith \"trivial bodies\" when accessed on the class object itself.\n\n\"Trivial bodies\" are bodies that solely consist of `...`, `pass`,\na docstring, and/or `raise NotImplementedError`.\n\n## Why is this bad?\n\nAn abstract method with a trivial body has no concrete implementation\nto execute, so calling such a method directly on the class will probably\nnot have the desired effect.\n\nIt is also unsound to call these methods directly on the class. Unlike\nother methods, ty permits abstract methods with trivial bodies to have\nnon-`None` return types even though they always return `None` at runtime.\nThis is because it is expected that these methods will always be\noverridden rather than being called directly. As a result of this\nexception to the normal rule, ty may infer an incorrect type if one of\nthese methods is called directly, which may then mean that type errors\nelsewhere in your code go undetected by ty.\n\nCalling abstract classmethods or staticmethods via `type[X]` is allowed,\nsince the actual runtime type could be a concrete subclass with an implementation.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\n\n\nclass Foo(ABC):\n @classmethod\n @abstractmethod\n def method(cls) -> int: ...\n\n\n# cannot call abstract classmethod\nFoo.method() # error\n```",