From 7532155c9bdd5945f5b352e5df3aa39f95849758 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 18 Oct 2025 12:44:21 +0200 Subject: [PATCH] [ty] Add suggestion to unknown rule diagnostics, rename `unknown-rule` lint to `ignore-comment-unknown-rule` (#20948) --- crates/ty/docs/rules.md | 194 +++++++++--------- crates/ty/tests/cli/rule_selection.rs | 18 +- crates/ty_project/src/metadata/options.rs | 29 +-- .../mdtest/suppressions/ty_ignore.md | 10 +- .../{util/diagnostics.rs => diagnostic.rs} | 26 +++ crates/ty_python_semantic/src/lib.rs | 11 +- crates/ty_python_semantic/src/lint.rs | 40 +++- .../src/{util => }/subscript.rs | 2 +- crates/ty_python_semantic/src/suppression.rs | 38 +--- crates/ty_python_semantic/src/types.rs | 2 +- .../src/types/diagnostic.rs | 29 +-- .../src/types/infer/builder.rs | 4 +- crates/ty_python_semantic/src/types/tuple.rs | 2 +- crates/ty_python_semantic/src/util/mod.rs | 2 - .../e2e__commands__debug_command.snap | 2 +- ty.schema.json | 20 +- 16 files changed, 207 insertions(+), 222 deletions(-) rename crates/ty_python_semantic/src/{util/diagnostics.rs => diagnostic.rs} (86%) rename crates/ty_python_semantic/src/{util => }/subscript.rs (99%) delete mode 100644 crates/ty_python_semantic/src/util/mod.rs diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 7687ea2a7c..858f1f0c7c 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -217,7 +217,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -329,7 +329,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -359,7 +359,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -474,7 +474,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -599,7 +599,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -650,7 +650,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -679,7 +679,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -723,7 +723,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -756,7 +756,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -787,7 +787,7 @@ alice["height"] # KeyError: 'height' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +822,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -856,7 +856,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -888,7 +888,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -938,7 +938,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -964,7 +964,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -998,7 +998,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1047,7 +1047,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1072,7 +1072,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1130,7 +1130,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1157,7 +1157,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1187,7 +1187,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1217,7 +1217,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1251,7 +1251,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1285,7 +1285,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1320,7 +1320,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1345,7 +1345,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1378,7 +1378,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1407,7 +1407,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1431,7 +1431,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1457,7 +1457,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1484,7 +1484,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1542,7 +1542,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1572,7 +1572,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1601,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1628,7 +1628,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1656,7 +1656,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1702,7 +1702,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1729,7 +1729,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1757,7 +1757,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1782,7 +1782,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1807,7 +1807,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1844,7 +1844,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1872,7 +1872,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1897,7 +1897,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1938,7 +1938,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -1959,6 +1959,37 @@ def old_func(): ... old_func() # emits [deprecated] diagnostic ``` +## `ignore-comment-unknown-rule` + + +Default level: warn · +Added in 0.0.1-alpha.1 · +Related issues · +View source + + + +**What it does** + +Checks for `ty: ignore[code]` where `code` isn't a known lint rule. + +**Why is this bad?** + +A `ty: ignore[code]` directive with a `code` that doesn't match +any known rule will not suppress any type errors, and is probably a mistake. + +**Examples** + +```py +a = 20 / 0 # ty: ignore[division-by-zer] +``` + +Use instead: + +```py +a = 20 / 0 # ty: ignore[division-by-zero] +``` + ## `invalid-ignore-comment` @@ -1995,7 +2026,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2023,7 +2054,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2055,7 +2086,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2087,7 +2118,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2114,7 +2145,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2132,44 +2163,13 @@ Using `reveal_type` without importing it will raise a `NameError` at runtime. reveal_type(1) # NameError: name 'reveal_type' is not defined ``` -## `unknown-rule` - - -Default level: warn · -Added in 0.0.1-alpha.1 · -Related issues · -View source - - - -**What it does** - -Checks for `ty: ignore[code]` where `code` isn't a known lint rule. - -**Why is this bad?** - -A `ty: ignore[code]` directive with a `code` that doesn't match -any known rule will not suppress any type errors, and is probably a mistake. - -**Examples** - -```py -a = 20 / 0 # ty: ignore[division-by-zer] -``` - -Use instead: - -```py -a = 20 / 0 # ty: ignore[division-by-zero] -``` - ## `unresolved-global` Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2227,7 +2227,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2266,7 +2266,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2329,7 +2329,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2353,7 +2353,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index 8e9ee8e3af..4a4c26b69c 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -250,11 +250,11 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { ("test.py", "print(10)"), ])?; - assert_cmd_snapshot!(case.command(), @r###" + assert_cmd_snapshot!(case.command(), @r#" success: true exit_code: 0 ----- stdout ----- - warning[unknown-rule]: Unknown lint rule `division-by-zer` + warning[unknown-rule]: Unknown rule `division-by-zer`. Did you mean `division-by-zero`? --> pyproject.toml:3:1 | 2 | [tool.ty.rules] @@ -265,7 +265,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { Found 1 diagnostic ----- stderr ----- - "###); + "#); Ok(()) } @@ -275,16 +275,16 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { fn cli_unknown_rules() -> anyhow::Result<()> { let case = CliTest::with_file("test.py", "print(10)")?; - assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r###" + assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" success: true exit_code: 0 ----- stdout ----- - warning[unknown-rule]: Unknown lint rule `division-by-zer` + warning[unknown-rule]: Unknown rule `division-by-zer`. Did you mean `division-by-zero`? Found 1 diagnostic ----- stderr ----- - "###); + "); Ok(()) } @@ -852,7 +852,7 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { ), ])?; - assert_cmd_snapshot!(case.command(), @r###" + assert_cmd_snapshot!(case.command(), @r#" success: false exit_code: 1 ----- stdout ----- @@ -864,7 +864,7 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { | info: rule `division-by-zero` was selected in the configuration file - warning[unknown-rule]: Unknown lint rule `division-by-zer` + warning[unknown-rule]: Unknown rule `division-by-zer`. Did you mean `division-by-zero`? --> pyproject.toml:10:1 | 8 | [tool.ty.overrides.rules] @@ -884,7 +884,7 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { Found 3 diagnostics ----- stderr ----- - "###); + "#); Ok(()) } diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 12b76affc1..08a1582f93 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -28,7 +28,7 @@ use std::ops::Deref; use std::sync::Arc; use thiserror::Error; use ty_combine::Combine; -use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; +use ty_python_semantic::lint::{Level, LintSource, RuleSelection}; use ty_python_semantic::{ ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError, @@ -840,28 +840,11 @@ impl Rules { .and_then(|path| system_path_to_file(db, path).ok()); // TODO: Add a note if the value was configured on the CLI - let diagnostic = match error { - GetLintError::Unknown(_) => OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!("Unknown lint rule `{rule_name}`"), - Severity::Warning, - ), - GetLintError::PrefixedWithCategory { suggestion, .. } => { - OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!( - "Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?" - ), - Severity::Warning, - ) - } - - GetLintError::Removed(_) => OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!("Unknown lint rule `{rule_name}`"), - Severity::Warning, - ), - }; + let diagnostic = OptionDiagnostic::new( + DiagnosticId::UnknownRule, + error.to_string(), + Severity::Warning, + ); let annotation = file.map(Span::from).map(|span| { Annotation::primary(span.with_optional_range(rule_name.range())) 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 9a1930f015..765d4f9e65 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -88,7 +88,7 @@ def test($): # ty: ignore ```py a = 10 # revealed: Literal[10] -# error: [unknown-rule] "Unknown rule `revealed-type`" +# error: [ignore-comment-unknown-rule] "Unknown rule `revealed-type`" reveal_type(a) # ty: ignore[revealed-type] ``` @@ -127,7 +127,7 @@ a = 10 / 0 # ty: ignore[*-*] ```py -a = 10 / 0 # ty: ignore[division-by-zero] +a = 10 / 0 # ty: ignore[division-by-zero] # ^^^^^^ trailing whitespace ``` @@ -178,14 +178,14 @@ a = 4 / 0 # error: [division-by-zero] ## Unknown rule ```py -# error: [unknown-rule] "Unknown rule `is-equal-14`" -a = 10 + 4 # ty: ignore[is-equal-14] +# error: [ignore-comment-unknown-rule] "Unknown rule `division-by-zer`. Did you mean `division-by-zero`?" +a = 10 + 4 # ty: ignore[division-by-zer] ``` ## Code with `lint:` prefix ```py -# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" +# error:[ignore-comment-unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" # error: [division-by-zero] a = 10 / 0 # ty: ignore[lint:division-by-zero] ``` diff --git a/crates/ty_python_semantic/src/util/diagnostics.rs b/crates/ty_python_semantic/src/diagnostic.rs similarity index 86% rename from crates/ty_python_semantic/src/util/diagnostics.rs rename to crates/ty_python_semantic/src/diagnostic.rs index 82ce6b1c3a..5936d0874d 100644 --- a/crates/ty_python_semantic/src/util/diagnostics.rs +++ b/crates/ty_python_semantic/src/diagnostic.rs @@ -1,3 +1,29 @@ +/// Suggest a name from `existing_names` that is similar to `wrong_name`. +pub(crate) fn did_you_mean, T: AsRef>( + existing_names: impl Iterator, + wrong_name: T, +) -> Option { + if wrong_name.as_ref().len() < 3 { + return None; + } + + existing_names + .filter(|ref id| id.as_ref().len() >= 2) + .map(|ref id| { + ( + id.as_ref().to_string(), + strsim::damerau_levenshtein( + &id.as_ref().to_lowercase(), + &wrong_name.as_ref().to_lowercase(), + ), + ) + }) + .min_by_key(|(_, dist)| *dist) + // Heuristic to filter out bad matches + .filter(|(_, dist)| *dist <= 3) + .map(|(id, _)| id) +} + use crate::{Db, Program, PythonVersionWithSource}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use std::fmt::Write; diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index dbe07aa600..5f41200522 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -5,8 +5,11 @@ use std::hash::BuildHasherDefault; use crate::lint::{LintRegistry, LintRegistryBuilder}; -use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COMMENT}; +use crate::suppression::{ + IGNORE_COMMENT_UNKNOWN_RULE, INVALID_IGNORE_COMMENT, UNUSED_IGNORE_COMMENT, +}; pub use db::Db; +pub use diagnostic::add_inferred_python_version_hint_to_diagnostic; pub use module_name::{ModuleName, ModuleNameResolutionError}; pub use module_resolver::{ Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules, @@ -27,7 +30,6 @@ pub use types::ide_support::{ ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_imported_symbol, definitions_for_name, map_stub_definition, }; -pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic; pub mod ast_node_ref; mod db; @@ -44,11 +46,12 @@ mod rank; pub mod semantic_index; mod semantic_model; pub(crate) mod site_packages; +mod subscript; mod suppression; pub mod types; mod unpack; -mod util; +mod diagnostic; #[cfg(feature = "testing")] pub mod pull_types; @@ -72,6 +75,6 @@ pub fn default_lint_registry() -> &'static LintRegistry { pub fn register_lints(registry: &mut LintRegistryBuilder) { types::register_lints(registry); registry.register_lint(&UNUSED_IGNORE_COMMENT); - registry.register_lint(&UNKNOWN_RULE); + registry.register_lint(&IGNORE_COMMENT_UNKNOWN_RULE); registry.register_lint(&INVALID_IGNORE_COMMENT); } diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index 6432d56956..0eccfac326 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -1,10 +1,11 @@ +use crate::diagnostic::did_you_mean; use core::fmt; use itertools::Itertools; use ruff_db::diagnostic::{DiagnosticId, LintName, Severity}; use rustc_hash::FxHashMap; +use std::error::Error; use std::fmt::Formatter; use std::hash::Hasher; -use thiserror::Error; #[derive(Debug, Clone)] pub struct LintMetadata { @@ -380,7 +381,12 @@ impl LintRegistry { } } - Err(GetLintError::Unknown(code.to_string())) + let suggestion = did_you_mean(self.by_name.keys(), code); + + Err(GetLintError::Unknown { + code: code.to_string(), + suggestion, + }) } } } @@ -415,25 +421,45 @@ impl LintRegistry { } } -#[derive(Error, Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum GetLintError { /// The name maps to this removed lint. - #[error("lint `{0}` has been removed")] Removed(LintName), /// No lint with the given name is known. - #[error("unknown lint `{0}`")] - Unknown(String), + Unknown { + code: String, + suggestion: Option, + }, /// The name uses the full qualified diagnostic id `lint:` instead of just `rule`. /// The String is the name without the `lint:` category prefix. - #[error("unknown lint `{prefixed}`. Did you mean `{suggestion}`?")] PrefixedWithCategory { prefixed: String, suggestion: String, }, } +impl Error for GetLintError {} + +impl std::fmt::Display for GetLintError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + GetLintError::Removed(code) => write!(f, "Removed rule `{code}`"), + GetLintError::Unknown { code, suggestion } => match suggestion { + None => write!(f, "Unknown rule `{code}`"), + Some(suggestion) => { + write!(f, "Unknown rule `{code}`. Did you mean `{suggestion}`?") + } + }, + GetLintError::PrefixedWithCategory { + prefixed, + suggestion, + } => write!(f, "Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"), + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum LintEntry { /// An existing lint rule. Can be in preview, stable or deprecated. diff --git a/crates/ty_python_semantic/src/util/subscript.rs b/crates/ty_python_semantic/src/subscript.rs similarity index 99% rename from crates/ty_python_semantic/src/util/subscript.rs rename to crates/ty_python_semantic/src/subscript.rs index f519acffdf..b7ea13db10 100644 --- a/crates/ty_python_semantic/src/util/subscript.rs +++ b/crates/ty_python_semantic/src/subscript.rs @@ -208,7 +208,7 @@ where mod tests { use crate::Db; use crate::db::tests::setup_db; - use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; + use crate::subscript::{OutOfBoundsError, StepSizeZeroError}; use super::{PyIndex, PySlice}; use itertools::{Itertools, assert_equal}; diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index cf33190c82..6c9edbec68 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -1,7 +1,7 @@ use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus}; use crate::types::TypeCheckDiagnostics; use crate::{Db, declare_lint, lint::LintId}; -use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Span}; +use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Span}; use ruff_db::{files::File, parsed::parsed_module, source::source_text}; use ruff_python_parser::TokenKind; use ruff_python_trivia::Cursor; @@ -55,7 +55,7 @@ declare_lint! { /// ```py /// a = 20 / 0 # ty: ignore[division-by-zero] /// ``` - pub(crate) static UNKNOWN_RULE = { + pub(crate) static IGNORE_COMMENT_UNKNOWN_RULE = { summary: "detects `ty: ignore` comments that reference unknown rules", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Warn, @@ -143,38 +143,12 @@ pub(crate) fn check_suppressions(db: &dyn Db, file: File, diagnostics: &mut Type /// Checks for `ty: ignore` comments that reference unknown rules. fn check_unknown_rule(context: &mut CheckSuppressionsContext) { - if context.is_lint_disabled(&UNKNOWN_RULE) { + if context.is_lint_disabled(&IGNORE_COMMENT_UNKNOWN_RULE) { return; } for unknown in &context.suppressions.unknown { - match &unknown.reason { - GetLintError::Removed(removed) => { - context.report_lint( - &UNKNOWN_RULE, - unknown.range, - format_args!("Removed rule `{removed}`"), - ); - } - GetLintError::Unknown(rule) => { - context.report_lint( - &UNKNOWN_RULE, - unknown.range, - format_args!("Unknown rule `{rule}`"), - ); - } - - GetLintError::PrefixedWithCategory { - prefixed, - suggestion, - } => { - context.report_lint( - &UNKNOWN_RULE, - unknown.range, - format_args!("Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"), - ); - } - } + context.report_lint(&IGNORE_COMMENT_UNKNOWN_RULE, unknown.range, &unknown.reason); } } @@ -300,7 +274,7 @@ impl<'a> CheckSuppressionsContext<'a> { &mut self, lint: &'static LintMetadata, range: TextRange, - message: fmt::Arguments, + message: impl IntoDiagnosticMessage, ) { if let Some(suppression) = self.suppressions.find_suppression(range, LintId::of(lint)) { self.diagnostics.mark_used(suppression.id()); @@ -316,7 +290,7 @@ impl<'a> CheckSuppressionsContext<'a> { &mut self, lint: &'static LintMetadata, range: TextRange, - message: fmt::Arguments, + message: impl IntoDiagnosticMessage, ) { let Some(severity) = self.db.rule_selection(self.file).severity(LintId::of(lint)) else { return; diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 477bf9aa11..6976d6dc4a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -30,6 +30,7 @@ pub(crate) use self::infer::{ }; pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; +pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, resolve_module}; use crate::place::{ @@ -69,7 +70,6 @@ pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_t use crate::types::variance::{TypeVarVariance, VarianceInferable}; use crate::types::visitor::any_over_type; use crate::unpack::EvaluationMode; -pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; use instance::Protocol; diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 8c836c0038..8046297079 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -5,6 +5,8 @@ use super::{ CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass, add_inferred_python_version_hint_to_diagnostic, }; +use crate::diagnostic::did_you_mean; +use crate::diagnostic::format_enumeration; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; @@ -23,7 +25,6 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, infer_isolated_expression, protocol_class::ProtocolClass, }; -use crate::util::diagnostics::format_enumeration; use crate::{ Db, DisplaySettings, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint, }; @@ -3190,29 +3191,3 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( &format!("accessing `{}`", attr.id), ); } - -/// Suggest a name from `existing_names` that is similar to `wrong_name`. -fn did_you_mean, T: AsRef>( - existing_names: impl Iterator, - wrong_name: T, -) -> Option { - if wrong_name.as_ref().len() < 3 { - return None; - } - - existing_names - .filter(|ref id| id.as_ref().len() >= 2) - .map(|ref id| { - ( - id.as_ref().to_string(), - strsim::damerau_levenshtein( - &id.as_ref().to_lowercase(), - &wrong_name.as_ref().to_lowercase(), - ), - ) - }) - .min_by_key(|(_, dist)| *dist) - // Heuristic to filter out bad matches - .filter(|(_, dist)| *dist <= 3) - .map(|(id, _)| id) -} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 2dee39100e..f0fd9ec502 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -17,6 +17,7 @@ use super::{ infer_deferred_types, infer_definition_types, infer_expression_types, infer_same_file_expression_type, infer_scope_types, infer_unpack_types, }; +use crate::diagnostic::format_enumeration; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::{ KnownModule, ModuleResolveMode, file_to_module, resolve_module, search_paths, @@ -45,6 +46,7 @@ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, }; +use crate::subscript::{PyIndex, PySlice}; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator}; @@ -104,8 +106,6 @@ use crate::types::{ }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; -use crate::util::diagnostics::format_enumeration; -use crate::util::subscript::{PyIndex, PySlice}; use crate::{Db, FxOrderSet, Program}; mod annotation_expression; diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index dcb3df675d..f091c99ea5 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -22,6 +22,7 @@ use std::hash::Hash; use itertools::{Either, EitherOrBoth, Itertools}; use crate::semantic_index::definition::Definition; +use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; @@ -31,7 +32,6 @@ use crate::types::{ UnionBuilder, UnionType, }; use crate::types::{Truthiness, TypeContext}; -use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::{Db, FxOrderSet, Program}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/ty_python_semantic/src/util/mod.rs b/crates/ty_python_semantic/src/util/mod.rs deleted file mode 100644 index 54555cd617..0000000000 --- a/crates/ty_python_semantic/src/util/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod diagnostics; -pub(crate) mod subscript; diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 0a9007459d..ba3b75028c 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -40,6 +40,7 @@ Settings: Settings { "duplicate-kw-only": Error (Default), "escape-character-in-forward-annotation": Error (Default), "fstring-type-annotation": Error (Default), + "ignore-comment-unknown-rule": Warning (Default), "implicit-concatenated-string-type-annotation": Error (Default), "inconsistent-mro": Error (Default), "index-out-of-bounds": Error (Default), @@ -90,7 +91,6 @@ Settings: Settings { "unavailable-implicit-super-arguments": Error (Default), "undefined-reveal": Warning (Default), "unknown-argument": Error (Default), - "unknown-rule": Warning (Default), "unresolved-attribute": Error (Default), "unresolved-global": Warning (Default), "unresolved-import": Error (Default), diff --git a/ty.schema.json b/ty.schema.json index 5e3323b517..55cb190bb8 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -415,6 +415,16 @@ } ] }, + "ignore-comment-unknown-rule": { + "title": "detects `ty: ignore` comments that reference unknown rules", + "description": "## What it does\nChecks for `ty: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `ty: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # ty: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # ty: ignore[division-by-zero]\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "implicit-concatenated-string-type-annotation": { "title": "detects implicit concatenated strings in type annotations", "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyze type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n ...\n```", @@ -925,16 +935,6 @@ } ] }, - "unknown-rule": { - "title": "detects `ty: ignore` comments that reference unknown rules", - "description": "## What it does\nChecks for `ty: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `ty: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # ty: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # ty: ignore[division-by-zero]\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, "unresolved-attribute": { "title": "detects references to unresolved attributes", "description": "## What it does\nChecks for unresolved attributes.\n\n## Why is this bad?\nAccessing an unbound attribute will raise an `AttributeError` at runtime.\nAn unresolved attribute is not guaranteed to exist from the type alone,\nso this could also indicate that the object is not of the type that the user expects.\n\n## Examples\n```python\nclass A: ...\n\nA().foo # AttributeError: 'A' object has no attribute 'foo'\n```",