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```",