Compare commits
1 Commits
david/hasa
...
david/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49c077d5d4 |
@@ -78,7 +78,7 @@ fn setup_tomllib_case() -> Case {
|
||||
|
||||
let src_root = SystemPath::new("/src");
|
||||
let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap();
|
||||
metadata.apply_options(Options {
|
||||
metadata.apply_cli_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
@@ -224,7 +224,7 @@ fn setup_micro_case(code: &str) -> Case {
|
||||
|
||||
let src_root = SystemPath::new("/src");
|
||||
let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap();
|
||||
metadata.apply_options(Options {
|
||||
metadata.apply_cli_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
|
||||
@@ -91,99 +91,3 @@ _ = "\8""0" # fix should be "\80"
|
||||
_ = "\12""8" # fix should be "\128"
|
||||
_ = "\12""foo" # fix should be "\12foo"
|
||||
_ = "\12" "" # fix should be "\12"
|
||||
|
||||
|
||||
# Mixed literal + non-literal scenarios
|
||||
_ = (
|
||||
"start" +
|
||||
variable +
|
||||
"end"
|
||||
)
|
||||
|
||||
_ = (
|
||||
f"format" +
|
||||
func_call() +
|
||||
"literal"
|
||||
)
|
||||
|
||||
_ = (
|
||||
rf"raw_f{x}" +
|
||||
r"raw_normal"
|
||||
)
|
||||
|
||||
|
||||
# Different prefix combinations
|
||||
_ = (
|
||||
u"unicode" +
|
||||
r"raw"
|
||||
)
|
||||
|
||||
_ = (
|
||||
rb"raw_bytes" +
|
||||
b"normal_bytes"
|
||||
)
|
||||
|
||||
_ = (
|
||||
b"bytes" +
|
||||
b"with_bytes"
|
||||
)
|
||||
|
||||
# Repeated concatenation
|
||||
|
||||
_ = ("a" +
|
||||
"b" +
|
||||
"c" +
|
||||
"d" + "e"
|
||||
)
|
||||
|
||||
_ = ("a"
|
||||
+ "b"
|
||||
+ "c"
|
||||
+ "d"
|
||||
+ "e"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"start" +
|
||||
variable + # comment
|
||||
"end"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"start" +
|
||||
variable
|
||||
# leading comment
|
||||
+ "end"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"first"
|
||||
+ "second" # extra spaces around +
|
||||
)
|
||||
|
||||
_ = (
|
||||
"first" + # trailing spaces before +
|
||||
"second"
|
||||
)
|
||||
|
||||
_ = ((
|
||||
"deep" +
|
||||
"nesting"
|
||||
))
|
||||
|
||||
_ = (
|
||||
"contains + plus" +
|
||||
"another string"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"start"
|
||||
# leading comment
|
||||
+ "end"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"start" +
|
||||
# leading comment
|
||||
"end"
|
||||
)
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
class A:
|
||||
...
|
||||
|
||||
|
||||
class A(metaclass=type):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
metaclass=type
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
metaclass=type
|
||||
#
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
#
|
||||
metaclass=type
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
metaclass=type,
|
||||
#
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
#
|
||||
metaclass=type,
|
||||
#
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class B(A, metaclass=type):
|
||||
...
|
||||
|
||||
|
||||
class B(
|
||||
A,
|
||||
metaclass=type,
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
class B(
|
||||
A,
|
||||
# comment
|
||||
metaclass=type,
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
def foo():
|
||||
class A(metaclass=type):
|
||||
...
|
||||
|
||||
|
||||
class A(
|
||||
metaclass=type # comment
|
||||
,
|
||||
):
|
||||
...
|
||||
|
||||
|
||||
type = str
|
||||
|
||||
class Foo(metaclass=type):
|
||||
...
|
||||
|
||||
|
||||
import builtins
|
||||
|
||||
class A(metaclass=builtins.type):
|
||||
...
|
||||
@@ -1364,8 +1364,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
op: Operator::Add, ..
|
||||
}) => {
|
||||
if checker.enabled(Rule::ExplicitStringConcatenation) {
|
||||
if let Some(diagnostic) = flake8_implicit_str_concat::rules::explicit(expr, checker)
|
||||
{
|
||||
if let Some(diagnostic) = flake8_implicit_str_concat::rules::explicit(
|
||||
expr,
|
||||
checker.locator,
|
||||
checker.settings,
|
||||
) {
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,9 +439,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UselessObjectInheritance) {
|
||||
pyupgrade::rules::useless_object_inheritance(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::UselessClassMetaclassType) {
|
||||
pyupgrade::rules::useless_class_metaclass_type(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::ReplaceStrEnum) {
|
||||
if checker.target_version() >= PythonVersion::PY311 {
|
||||
pyupgrade::rules::replace_str_enum(checker, class_def);
|
||||
|
||||
@@ -552,7 +552,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass),
|
||||
(Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction),
|
||||
(Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter),
|
||||
(Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType),
|
||||
|
||||
// pydocstyle
|
||||
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use ruff_diagnostics::AlwaysFixableViolation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::Locator;
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for string literals that are explicitly concatenated (using the
|
||||
@@ -35,76 +34,46 @@ use crate::checkers::ast::Checker;
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ExplicitStringConcatenation;
|
||||
|
||||
impl AlwaysFixableViolation for ExplicitStringConcatenation {
|
||||
impl Violation for ExplicitStringConcatenation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Explicitly concatenated string should be implicitly concatenated".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove redundant '+' operator to implicitly concatenate".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ISC003
|
||||
pub(crate) fn explicit(expr: &Expr, checker: &Checker) -> Option<Diagnostic> {
|
||||
pub(crate) fn explicit(
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
settings: &LinterSettings,
|
||||
) -> Option<Diagnostic> {
|
||||
// If the user sets `allow-multiline` to `false`, then we should allow explicitly concatenated
|
||||
// strings that span multiple lines even if this rule is enabled. Otherwise, there's no way
|
||||
// for the user to write multiline strings, and that setting is "more explicit" than this rule
|
||||
// being enabled.
|
||||
if !checker.settings.flake8_implicit_str_concat.allow_multiline {
|
||||
if !settings.flake8_implicit_str_concat.allow_multiline {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Expr::BinOp(bin_op) = expr {
|
||||
if let ast::ExprBinOp {
|
||||
left,
|
||||
right,
|
||||
op: Operator::Add,
|
||||
..
|
||||
} = bin_op
|
||||
{
|
||||
let concatable = matches!(
|
||||
(left.as_ref(), right.as_ref()),
|
||||
(
|
||||
Expr::StringLiteral(_) | Expr::FString(_),
|
||||
Expr::StringLiteral(_) | Expr::FString(_)
|
||||
) | (Expr::BytesLiteral(_), Expr::BytesLiteral(_))
|
||||
);
|
||||
if concatable
|
||||
&& checker
|
||||
.locator()
|
||||
.contains_line_break(TextRange::new(left.end(), right.start()))
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op,
|
||||
right,
|
||||
range,
|
||||
}) = expr
|
||||
{
|
||||
if matches!(op, Operator::Add) {
|
||||
if matches!(
|
||||
left.as_ref(),
|
||||
Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_)
|
||||
) && matches!(
|
||||
right.as_ref(),
|
||||
Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_)
|
||||
) && locator.contains_line_break(*range)
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(ExplicitStringConcatenation, expr.range());
|
||||
diagnostic.set_fix(generate_fix(checker, bin_op));
|
||||
return Some(diagnostic);
|
||||
return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Fix {
|
||||
let ast::ExprBinOp { left, right, .. } = expr_bin_op;
|
||||
|
||||
let between_operands_range = TextRange::new(left.end(), right.start());
|
||||
let between_operands = checker.locator().slice(between_operands_range);
|
||||
let (before_plus, after_plus) = between_operands.split_once('+').unwrap();
|
||||
|
||||
let linebreak_before_operator =
|
||||
before_plus.contains_line_break(TextRange::at(TextSize::new(0), before_plus.text_len()));
|
||||
|
||||
// If removing `+` from first line trim trailing spaces
|
||||
// Preserve indentation when removing `+` from second line
|
||||
let before_plus = if linebreak_before_operator {
|
||||
before_plus
|
||||
} else {
|
||||
before_plus.trim_end_matches(is_python_whitespace)
|
||||
};
|
||||
|
||||
Fix::safe_edit(Edit::range_replacement(
|
||||
format!("{before_plus}{after_plus}"),
|
||||
between_operands_range,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -461,7 +461,6 @@ ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
91 |+_ = "\128" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
94 94 |
|
||||
|
||||
ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
@@ -480,8 +479,6 @@ ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
92 |-_ = "\12""foo" # fix should be "\12foo"
|
||||
92 |+_ = "\12foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
94 94 |
|
||||
95 95 |
|
||||
|
||||
ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
@@ -498,6 +495,3 @@ ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 |-_ = "\12" "" # fix should be "\12"
|
||||
93 |+_ = "\12" # fix should be "\12"
|
||||
94 94 |
|
||||
95 95 |
|
||||
96 96 | # Mixed literal + non-literal scenarios
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
ISC.py:9:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
ISC.py:9:3: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
8 | _ = (
|
||||
9 | / "abc" +
|
||||
@@ -9,19 +9,8 @@ ISC.py:9:3: ISC003 [*] Explicitly concatenated string should be implicitly conca
|
||||
| |_______^ ISC003
|
||||
11 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | "def"
|
||||
7 7 |
|
||||
8 8 | _ = (
|
||||
9 |- "abc" +
|
||||
9 |+ "abc"
|
||||
10 10 | "def"
|
||||
11 11 | )
|
||||
12 12 |
|
||||
|
||||
ISC.py:14:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
ISC.py:14:3: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
13 | _ = (
|
||||
14 | / f"abc" +
|
||||
@@ -29,19 +18,8 @@ ISC.py:14:3: ISC003 [*] Explicitly concatenated string should be implicitly conc
|
||||
| |_______^ ISC003
|
||||
16 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | )
|
||||
12 12 |
|
||||
13 13 | _ = (
|
||||
14 |- f"abc" +
|
||||
14 |+ f"abc"
|
||||
15 15 | "def"
|
||||
16 16 | )
|
||||
17 17 |
|
||||
|
||||
ISC.py:19:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
ISC.py:19:3: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
18 | _ = (
|
||||
19 | / b"abc" +
|
||||
@@ -49,19 +27,8 @@ ISC.py:19:3: ISC003 [*] Explicitly concatenated string should be implicitly conc
|
||||
| |________^ ISC003
|
||||
21 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | )
|
||||
17 17 |
|
||||
18 18 | _ = (
|
||||
19 |- b"abc" +
|
||||
19 |+ b"abc"
|
||||
20 20 | b"def"
|
||||
21 21 | )
|
||||
22 22 |
|
||||
|
||||
ISC.py:78:10: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
ISC.py:78:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
77 | # Explicitly concatenated nested f-strings
|
||||
78 | _ = f"a {f"first"
|
||||
@@ -71,19 +38,8 @@ ISC.py:78:10: ISC003 [*] Explicitly concatenated string should be implicitly con
|
||||
80 | _ = f"a {f"first {f"middle"}"
|
||||
81 | + f"second"} d"
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
76 76 |
|
||||
77 77 | # Explicitly concatenated nested f-strings
|
||||
78 78 | _ = f"a {f"first"
|
||||
79 |- + f"second"} d"
|
||||
79 |+ f"second"} d"
|
||||
80 80 | _ = f"a {f"first {f"middle"}"
|
||||
81 81 | + f"second"} d"
|
||||
82 82 |
|
||||
|
||||
ISC.py:80:10: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
78 | _ = f"a {f"first"
|
||||
79 | + f"second"} d"
|
||||
@@ -94,263 +50,3 @@ ISC.py:80:10: ISC003 [*] Explicitly concatenated string should be implicitly con
|
||||
82 |
|
||||
83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
78 78 | _ = f"a {f"first"
|
||||
79 79 | + f"second"} d"
|
||||
80 80 | _ = f"a {f"first {f"middle"}"
|
||||
81 |- + f"second"} d"
|
||||
81 |+ f"second"} d"
|
||||
82 82 |
|
||||
83 83 | # See https://github.com/astral-sh/ruff/issues/12936
|
||||
84 84 | _ = "\12""0" # fix should be "\0120"
|
||||
|
||||
ISC.py:110:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
109 | _ = (
|
||||
110 | / rf"raw_f{x}" +
|
||||
111 | | r"raw_normal"
|
||||
| |_________________^ ISC003
|
||||
112 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
107 107 | )
|
||||
108 108 |
|
||||
109 109 | _ = (
|
||||
110 |- rf"raw_f{x}" +
|
||||
110 |+ rf"raw_f{x}"
|
||||
111 111 | r"raw_normal"
|
||||
112 112 | )
|
||||
113 113 |
|
||||
|
||||
ISC.py:117:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
115 | # Different prefix combinations
|
||||
116 | _ = (
|
||||
117 | / u"unicode" +
|
||||
118 | | r"raw"
|
||||
| |__________^ ISC003
|
||||
119 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
114 114 |
|
||||
115 115 | # Different prefix combinations
|
||||
116 116 | _ = (
|
||||
117 |- u"unicode" +
|
||||
117 |+ u"unicode"
|
||||
118 118 | r"raw"
|
||||
119 119 | )
|
||||
120 120 |
|
||||
|
||||
ISC.py:122:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
121 | _ = (
|
||||
122 | / rb"raw_bytes" +
|
||||
123 | | b"normal_bytes"
|
||||
| |___________________^ ISC003
|
||||
124 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
119 119 | )
|
||||
120 120 |
|
||||
121 121 | _ = (
|
||||
122 |- rb"raw_bytes" +
|
||||
122 |+ rb"raw_bytes"
|
||||
123 123 | b"normal_bytes"
|
||||
124 124 | )
|
||||
125 125 |
|
||||
|
||||
ISC.py:127:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
126 | _ = (
|
||||
127 | / b"bytes" +
|
||||
128 | | b"with_bytes"
|
||||
| |_________________^ ISC003
|
||||
129 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
124 124 | )
|
||||
125 125 |
|
||||
126 126 | _ = (
|
||||
127 |- b"bytes" +
|
||||
127 |+ b"bytes"
|
||||
128 128 | b"with_bytes"
|
||||
129 129 | )
|
||||
130 130 |
|
||||
|
||||
ISC.py:133:6: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
131 | # Repeated concatenation
|
||||
132 |
|
||||
133 | _ = ("a" +
|
||||
| ______^
|
||||
134 | | "b" +
|
||||
| |_______^ ISC003
|
||||
135 | "c" +
|
||||
136 | "d" + "e"
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
130 130 |
|
||||
131 131 | # Repeated concatenation
|
||||
132 132 |
|
||||
133 |-_ = ("a" +
|
||||
133 |+_ = ("a"
|
||||
134 134 | "b" +
|
||||
135 135 | "c" +
|
||||
136 136 | "d" + "e"
|
||||
|
||||
ISC.py:139:6: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
137 | )
|
||||
138 |
|
||||
139 | _ = ("a"
|
||||
| ______^
|
||||
140 | | + "b"
|
||||
| |_________^ ISC003
|
||||
141 | + "c"
|
||||
142 | + "d"
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
137 137 | )
|
||||
138 138 |
|
||||
139 139 | _ = ("a"
|
||||
140 |- + "b"
|
||||
140 |+ "b"
|
||||
141 141 | + "c"
|
||||
142 142 | + "d"
|
||||
143 143 | + "e"
|
||||
|
||||
ISC.py:160:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
159 | _ = (
|
||||
160 | / "first"
|
||||
161 | | + "second" # extra spaces around +
|
||||
| |_________________^ ISC003
|
||||
162 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
158 158 |
|
||||
159 159 | _ = (
|
||||
160 160 | "first"
|
||||
161 |- + "second" # extra spaces around +
|
||||
161 |+ "second" # extra spaces around +
|
||||
162 162 | )
|
||||
163 163 |
|
||||
164 164 | _ = (
|
||||
|
||||
ISC.py:165:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
164 | _ = (
|
||||
165 | / "first" + # trailing spaces before +
|
||||
166 | | "second"
|
||||
| |____________^ ISC003
|
||||
167 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
162 162 | )
|
||||
163 163 |
|
||||
164 164 | _ = (
|
||||
165 |- "first" + # trailing spaces before +
|
||||
165 |+ "first" # trailing spaces before +
|
||||
166 166 | "second"
|
||||
167 167 | )
|
||||
168 168 |
|
||||
|
||||
ISC.py:170:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
169 | _ = ((
|
||||
170 | / "deep" +
|
||||
171 | | "nesting"
|
||||
| |_____________^ ISC003
|
||||
172 | ))
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
167 167 | )
|
||||
168 168 |
|
||||
169 169 | _ = ((
|
||||
170 |- "deep" +
|
||||
170 |+ "deep"
|
||||
171 171 | "nesting"
|
||||
172 172 | ))
|
||||
173 173 |
|
||||
|
||||
ISC.py:175:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
174 | _ = (
|
||||
175 | / "contains + plus" +
|
||||
176 | | "another string"
|
||||
| |____________________^ ISC003
|
||||
177 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
172 172 | ))
|
||||
173 173 |
|
||||
174 174 | _ = (
|
||||
175 |- "contains + plus" +
|
||||
175 |+ "contains + plus"
|
||||
176 176 | "another string"
|
||||
177 177 | )
|
||||
178 178 |
|
||||
|
||||
ISC.py:180:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
179 | _ = (
|
||||
180 | / "start"
|
||||
181 | | # leading comment
|
||||
182 | | + "end"
|
||||
| |___________^ ISC003
|
||||
183 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
179 179 | _ = (
|
||||
180 180 | "start"
|
||||
181 181 | # leading comment
|
||||
182 |- + "end"
|
||||
182 |+ "end"
|
||||
183 183 | )
|
||||
184 184 |
|
||||
185 185 | _ = (
|
||||
|
||||
ISC.py:186:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
185 | _ = (
|
||||
186 | / "start" +
|
||||
187 | | # leading comment
|
||||
188 | | "end"
|
||||
| |_________^ ISC003
|
||||
189 | )
|
||||
|
|
||||
= help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ℹ Safe fix
|
||||
183 183 | )
|
||||
184 184 |
|
||||
185 185 | _ = (
|
||||
186 |- "start" +
|
||||
186 |+ "start"
|
||||
187 187 | # leading comment
|
||||
188 188 | "end"
|
||||
189 189 | )
|
||||
|
||||
@@ -461,7 +461,6 @@ ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
91 |+_ = "\128" # fix should be "\128"
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
94 94 |
|
||||
|
||||
ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
@@ -480,8 +479,6 @@ ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
92 |-_ = "\12""foo" # fix should be "\12foo"
|
||||
92 |+_ = "\12foo" # fix should be "\12foo"
|
||||
93 93 | _ = "\12" "" # fix should be "\12"
|
||||
94 94 |
|
||||
95 95 |
|
||||
|
||||
ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
@@ -498,6 +495,3 @@ ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
92 92 | _ = "\12""foo" # fix should be "\12foo"
|
||||
93 |-_ = "\12" "" # fix should be "\12"
|
||||
93 |+_ = "\12" # fix should be "\12"
|
||||
94 94 |
|
||||
95 95 |
|
||||
96 96 | # Mixed literal + non-literal scenarios
|
||||
|
||||
@@ -136,18 +136,22 @@ impl AlwaysFixableViolation for TrueFalseComparison {
|
||||
let cond = cond.truncated_display();
|
||||
match (value, op) {
|
||||
(true, EqCmpOp::Eq) => {
|
||||
format!("Avoid equality comparisons to `True`; use `{cond}:` for truth checks")
|
||||
format!("Avoid equality comparisons to `True`; use `if {cond}:` for truth checks")
|
||||
}
|
||||
(true, EqCmpOp::NotEq) => {
|
||||
format!(
|
||||
"Avoid inequality comparisons to `True`; use `not {cond}:` for false checks"
|
||||
"Avoid inequality comparisons to `True`; use `if not {cond}:` for false checks"
|
||||
)
|
||||
}
|
||||
(false, EqCmpOp::Eq) => {
|
||||
format!("Avoid equality comparisons to `False`; use `not {cond}:` for false checks")
|
||||
format!(
|
||||
"Avoid equality comparisons to `False`; use `if not {cond}:` for false checks"
|
||||
)
|
||||
}
|
||||
(false, EqCmpOp::NotEq) => {
|
||||
format!("Avoid inequality comparisons to `False`; use `{cond}:` for truth checks")
|
||||
format!(
|
||||
"Avoid inequality comparisons to `False`; use `if {cond}:` for truth checks"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `res:` for truth checks
|
||||
E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `if res:` for truth checks
|
||||
|
|
||||
1 | #: E712
|
||||
2 | if res == True:
|
||||
@@ -19,7 +19,7 @@ E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `res:` for truth
|
||||
4 4 | #: E712
|
||||
5 5 | if res != False:
|
||||
|
||||
E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `res:` for truth checks
|
||||
E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `if res:` for truth checks
|
||||
|
|
||||
3 | pass
|
||||
4 | #: E712
|
||||
@@ -40,7 +40,7 @@ E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `res:` for tr
|
||||
7 7 | #: E712
|
||||
8 8 | if True != res:
|
||||
|
||||
E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `not res:` for false checks
|
||||
E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `if not res:` for false checks
|
||||
|
|
||||
6 | pass
|
||||
7 | #: E712
|
||||
@@ -61,7 +61,7 @@ E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `not res:` for
|
||||
10 10 | #: E712
|
||||
11 11 | if False == res:
|
||||
|
||||
E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `not res:` for false checks
|
||||
E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `if not res:` for false checks
|
||||
|
|
||||
9 | pass
|
||||
10 | #: E712
|
||||
@@ -82,7 +82,7 @@ E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `not res:` for
|
||||
13 13 | #: E712
|
||||
14 14 | if res[1] == True:
|
||||
|
||||
E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `res[1]:` for truth checks
|
||||
E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `if res[1]:` for truth checks
|
||||
|
|
||||
12 | pass
|
||||
13 | #: E712
|
||||
@@ -103,7 +103,7 @@ E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `res[1]:` for t
|
||||
16 16 | #: E712
|
||||
17 17 | if res[1] != False:
|
||||
|
||||
E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `res[1]:` for truth checks
|
||||
E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `if res[1]:` for truth checks
|
||||
|
|
||||
15 | pass
|
||||
16 | #: E712
|
||||
@@ -124,7 +124,7 @@ E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `res[1]:` fo
|
||||
19 19 | #: E712
|
||||
20 20 | var = 1 if cond == True else -1 if cond == False else cond
|
||||
|
||||
E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `cond:` for truth checks
|
||||
E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `if cond:` for truth checks
|
||||
|
|
||||
18 | pass
|
||||
19 | #: E712
|
||||
@@ -145,7 +145,7 @@ E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `cond:` for tr
|
||||
22 22 | if (True) == TrueElement or x == TrueElement:
|
||||
23 23 | pass
|
||||
|
||||
E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `not cond:` for false checks
|
||||
E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `if not cond:` for false checks
|
||||
|
|
||||
18 | pass
|
||||
19 | #: E712
|
||||
@@ -166,7 +166,7 @@ E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `not cond:` f
|
||||
22 22 | if (True) == TrueElement or x == TrueElement:
|
||||
23 23 | pass
|
||||
|
||||
E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `TrueElement:` for truth checks
|
||||
E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement:` for truth checks
|
||||
|
|
||||
20 | var = 1 if cond == True else -1 if cond == False else cond
|
||||
21 | #: E712
|
||||
@@ -226,7 +226,7 @@ E712.py:25:4: E712 [*] Avoid equality comparisons to `True` or `False`
|
||||
27 27 |
|
||||
28 28 | if(True) == TrueElement or x == TrueElement:
|
||||
|
||||
E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `TrueElement:` for truth checks
|
||||
E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement:` for truth checks
|
||||
|
|
||||
26 | pass
|
||||
27 |
|
||||
@@ -246,7 +246,7 @@ E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `TrueElement:`
|
||||
30 30 |
|
||||
31 31 | if (yield i) == True:
|
||||
|
||||
E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `yield i:` for truth checks
|
||||
E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `if yield i:` for truth checks
|
||||
|
|
||||
29 | pass
|
||||
30 |
|
||||
@@ -266,7 +266,7 @@ E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `yield i:` for
|
||||
33 33 |
|
||||
34 34 | #: Okay
|
||||
|
||||
E712.py:58:4: E712 [*] Avoid equality comparisons to `True`; use `True:` for truth checks
|
||||
E712.py:58:4: E712 [*] Avoid equality comparisons to `True`; use `if True:` for truth checks
|
||||
|
|
||||
57 | # https://github.com/astral-sh/ruff/issues/17582
|
||||
58 | if True == True: # No duplicated diagnostic
|
||||
|
||||
@@ -106,7 +106,7 @@ constant_literals.py:12:4: F632 [*] Use `==` to compare constant literals
|
||||
14 14 | if False == None: # E711, E712 (fix)
|
||||
15 15 | pass
|
||||
|
||||
constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use `not None:` for false checks
|
||||
constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use `if not None:` for false checks
|
||||
|
|
||||
12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712)
|
||||
13 | pass
|
||||
@@ -168,7 +168,7 @@ constant_literals.py:16:4: E711 [*] Comparison to `None` should be `cond is None
|
||||
18 18 |
|
||||
19 19 | named_var = []
|
||||
|
||||
constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use `not None:` for false checks
|
||||
constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use `if not None:` for false checks
|
||||
|
|
||||
14 | if False == None: # E711, E712 (fix)
|
||||
15 | pass
|
||||
|
||||
@@ -111,7 +111,6 @@ mod tests {
|
||||
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
|
||||
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))]
|
||||
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))]
|
||||
#[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = path.to_string_lossy().to_string();
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -37,7 +37,6 @@ pub(crate) use unpacked_list_comprehension::*;
|
||||
pub(crate) use use_pep585_annotation::*;
|
||||
pub(crate) use use_pep604_annotation::*;
|
||||
pub(crate) use use_pep604_isinstance::*;
|
||||
pub(crate) use useless_class_metaclass_type::*;
|
||||
pub(crate) use useless_metaclass_type::*;
|
||||
pub(crate) use useless_object_inheritance::*;
|
||||
pub(crate) use yield_in_for_loop::*;
|
||||
@@ -81,7 +80,6 @@ mod unpacked_list_comprehension;
|
||||
mod use_pep585_annotation;
|
||||
mod use_pep604_annotation;
|
||||
mod use_pep604_isinstance;
|
||||
mod useless_class_metaclass_type;
|
||||
mod useless_metaclass_type;
|
||||
mod useless_object_inheritance;
|
||||
mod yield_in_for_loop;
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{Parentheses, remove_argument};
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::StmtClassDef;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `metaclass=type` in class definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Since Python 3, the default metaclass is `type`, so specifying it explicitly is redundant.
|
||||
///
|
||||
/// Even though `__prepare__` is not required, the default metaclass (`type`) implements it,
|
||||
/// for the convenience of subclasses calling it via `super()`.
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo(metaclass=type): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UselessClassMetaclassType {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for UselessClassMetaclassType {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UselessClassMetaclassType { name } = self;
|
||||
format!("Class `{name}` uses `metaclass=type`, which is redundant")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove `metaclass=type`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// UP050
|
||||
pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtClassDef) {
|
||||
let Some(arguments) = class_def.arguments.as_deref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for keyword in &arguments.keywords {
|
||||
if let (Some("metaclass"), expr) = (keyword.arg.as_deref(), &keyword.value) {
|
||||
if checker.semantic().match_builtin_expr(expr, "type") {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UselessClassMetaclassType {
|
||||
name: class_def.name.to_string(),
|
||||
},
|
||||
keyword.range(),
|
||||
);
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_argument(
|
||||
keyword,
|
||||
arguments,
|
||||
Parentheses::Remove,
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.map(Fix::safe_edit)
|
||||
});
|
||||
|
||||
checker.report_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP050.py:5:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
5 | class A(metaclass=type):
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
6 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | ...
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 |-class A(metaclass=type):
|
||||
5 |+class A:
|
||||
6 6 | ...
|
||||
7 7 |
|
||||
8 8 |
|
||||
|
||||
UP050.py:10:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
9 | class A(
|
||||
10 | metaclass=type
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
11 | ):
|
||||
12 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | ...
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 |-class A(
|
||||
10 |- metaclass=type
|
||||
11 |-):
|
||||
9 |+class A:
|
||||
12 10 | ...
|
||||
13 11 |
|
||||
14 12 |
|
||||
|
||||
UP050.py:16:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
15 | class A(
|
||||
16 | metaclass=type
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
17 | #
|
||||
18 | ):
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | ...
|
||||
13 13 |
|
||||
14 14 |
|
||||
15 |-class A(
|
||||
16 |- metaclass=type
|
||||
17 |- #
|
||||
18 |-):
|
||||
15 |+class A:
|
||||
19 16 | ...
|
||||
20 17 |
|
||||
21 18 |
|
||||
|
||||
UP050.py:24:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
22 | class A(
|
||||
23 | #
|
||||
24 | metaclass=type
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
25 | ):
|
||||
26 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 | ...
|
||||
20 20 |
|
||||
21 21 |
|
||||
22 |-class A(
|
||||
23 |- #
|
||||
24 |- metaclass=type
|
||||
25 |-):
|
||||
22 |+class A:
|
||||
26 23 | ...
|
||||
27 24 |
|
||||
28 25 |
|
||||
|
||||
UP050.py:30:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
29 | class A(
|
||||
30 | metaclass=type,
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
31 | #
|
||||
32 | ):
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | ...
|
||||
27 27 |
|
||||
28 28 |
|
||||
29 |-class A(
|
||||
30 |- metaclass=type,
|
||||
31 |- #
|
||||
32 |-):
|
||||
29 |+class A:
|
||||
33 30 | ...
|
||||
34 31 |
|
||||
35 32 |
|
||||
|
||||
UP050.py:38:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
36 | class A(
|
||||
37 | #
|
||||
38 | metaclass=type,
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
39 | #
|
||||
40 | ):
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | ...
|
||||
34 34 |
|
||||
35 35 |
|
||||
36 |-class A(
|
||||
37 |- #
|
||||
38 |- metaclass=type,
|
||||
39 |- #
|
||||
40 |-):
|
||||
36 |+class A:
|
||||
41 37 | ...
|
||||
42 38 |
|
||||
43 39 |
|
||||
|
||||
UP050.py:44:12: UP050 [*] Class `B` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
44 | class B(A, metaclass=type):
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
45 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
41 41 | ...
|
||||
42 42 |
|
||||
43 43 |
|
||||
44 |-class B(A, metaclass=type):
|
||||
44 |+class B(A):
|
||||
45 45 | ...
|
||||
46 46 |
|
||||
47 47 |
|
||||
|
||||
UP050.py:50:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
48 | class B(
|
||||
49 | A,
|
||||
50 | metaclass=type,
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
51 | ):
|
||||
52 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
47 47 |
|
||||
48 48 | class B(
|
||||
49 49 | A,
|
||||
50 |- metaclass=type,
|
||||
51 50 | ):
|
||||
52 51 | ...
|
||||
53 52 |
|
||||
|
||||
UP050.py:58:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
56 | A,
|
||||
57 | # comment
|
||||
58 | metaclass=type,
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
59 | ):
|
||||
60 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
54 54 |
|
||||
55 55 | class B(
|
||||
56 56 | A,
|
||||
57 |- # comment
|
||||
58 |- metaclass=type,
|
||||
59 57 | ):
|
||||
60 58 | ...
|
||||
61 59 |
|
||||
|
||||
UP050.py:69:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
68 | class A(
|
||||
69 | metaclass=type # comment
|
||||
| ^^^^^^^^^^^^^^ UP050
|
||||
70 | ,
|
||||
71 | ):
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | ...
|
||||
66 66 |
|
||||
67 67 |
|
||||
68 |-class A(
|
||||
69 |- metaclass=type # comment
|
||||
70 |- ,
|
||||
71 |-):
|
||||
68 |+class A:
|
||||
72 69 | ...
|
||||
73 70 |
|
||||
74 71 |
|
||||
|
||||
UP050.py:83:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant
|
||||
|
|
||||
81 | import builtins
|
||||
82 |
|
||||
83 | class A(metaclass=builtins.type):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ UP050
|
||||
84 | ...
|
||||
|
|
||||
= help: Remove `metaclass=type`
|
||||
|
||||
ℹ Safe fix
|
||||
80 80 |
|
||||
81 81 | import builtins
|
||||
82 82 |
|
||||
83 |-class A(metaclass=builtins.type):
|
||||
83 |+class A:
|
||||
84 84 | ...
|
||||
@@ -22,7 +22,7 @@ ty_server = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
|
||||
clap = { workspace = true, features = ["wrap_help", "string"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
|
||||
4
crates/ty/docs/cli.md
generated
4
crates/ty/docs/cli.md
generated
@@ -47,9 +47,7 @@ ty check [OPTIONS] [PATH]...
|
||||
overriding a specific configuration option.</p>
|
||||
<p>Overrides of individual settings using this option always take precedence
|
||||
over all configuration files.</p>
|
||||
</dd><dt id="ty-check--config-file"><a href="#ty-check--config-file"><code>--config-file</code></a> <i>path</i></dt><dd><p>The path to a <code>ty.toml</code> file to use for configuration.</p>
|
||||
<p>While ty configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>
|
||||
<p>May also be set with the <code>TY_CONFIG_FILE</code> environment variable.</p></dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--error-on-warning"><a href="#ty-check--error-on-warning"><code>--error-on-warning</code></a></dt><dd><p>Use exit code 1 if there are any warning-level diagnostics</p>
|
||||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times)</p>
|
||||
|
||||
38
crates/ty/docs/configuration.md
generated
38
crates/ty/docs/configuration.md
generated
@@ -1,6 +1,25 @@
|
||||
<!-- WARNING: This file is auto-generated (cargo dev generate-all). Update the doc comments on the 'Options' struct in 'crates/ty_project/src/metadata/options.rs' if you want to change anything here. -->
|
||||
|
||||
# Configuration
|
||||
#### `respect-ignore-files`
|
||||
|
||||
Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
`.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
Enabled by default.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty]
|
||||
respect-ignore-files = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `rules`
|
||||
|
||||
Configures the enabled rules and their severity.
|
||||
@@ -143,25 +162,6 @@ typeshed = "/path/to/custom/typeshed"
|
||||
|
||||
## `src`
|
||||
|
||||
#### `respect-ignore-files`
|
||||
|
||||
Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
`.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
Enabled by default.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.src]
|
||||
respect-ignore-files = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `root`
|
||||
|
||||
The root of the project, used for finding first-party modules.
|
||||
|
||||
108
crates/ty/docs/rules.md
generated
108
crates/ty/docs/rules.md
generated
@@ -52,7 +52,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L93)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L91)
|
||||
</details>
|
||||
|
||||
## `conflicting-argument-forms`
|
||||
@@ -83,7 +83,7 @@ f(int) # error
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L137)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135)
|
||||
</details>
|
||||
|
||||
## `conflicting-declarations`
|
||||
@@ -113,7 +113,7 @@ a = 1
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L163)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L161)
|
||||
</details>
|
||||
|
||||
## `conflicting-metaclass`
|
||||
@@ -144,7 +144,7 @@ class C(A, B): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L188)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L186)
|
||||
</details>
|
||||
|
||||
## `cyclic-class-definition`
|
||||
@@ -175,7 +175,7 @@ class B(A): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L214)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L212)
|
||||
</details>
|
||||
|
||||
## `duplicate-base`
|
||||
@@ -201,7 +201,7 @@ class B(A, A): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L258)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
|
||||
</details>
|
||||
|
||||
## `escape-character-in-forward-annotation`
|
||||
@@ -338,7 +338,7 @@ TypeError: multiple bases have instance lay-out conflict
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20incompatible-slots)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L279)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L277)
|
||||
</details>
|
||||
|
||||
## `inconsistent-mro`
|
||||
@@ -367,7 +367,7 @@ class C(A, B): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L365)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363)
|
||||
</details>
|
||||
|
||||
## `index-out-of-bounds`
|
||||
@@ -392,7 +392,7 @@ t[3] # IndexError: tuple index out of range
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L389)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L387)
|
||||
</details>
|
||||
|
||||
## `invalid-argument-type`
|
||||
@@ -418,7 +418,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L409)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L407)
|
||||
</details>
|
||||
|
||||
## `invalid-assignment`
|
||||
@@ -445,7 +445,7 @@ a: int = ''
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L447)
|
||||
</details>
|
||||
|
||||
## `invalid-attribute-access`
|
||||
@@ -478,7 +478,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1395)
|
||||
</details>
|
||||
|
||||
## `invalid-base`
|
||||
@@ -501,7 +501,7 @@ class A(42): ... # error: [invalid-base]
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L469)
|
||||
</details>
|
||||
|
||||
## `invalid-context-manager`
|
||||
@@ -527,7 +527,7 @@ with 1:
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L522)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L520)
|
||||
</details>
|
||||
|
||||
## `invalid-declaration`
|
||||
@@ -555,7 +555,7 @@ a: str
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541)
|
||||
</details>
|
||||
|
||||
## `invalid-exception-caught`
|
||||
@@ -596,7 +596,7 @@ except ZeroDivisionError:
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L566)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L564)
|
||||
</details>
|
||||
|
||||
## `invalid-generic-class`
|
||||
@@ -627,7 +627,7 @@ class C[U](Generic[T]): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L602)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L600)
|
||||
</details>
|
||||
|
||||
## `invalid-legacy-type-variable`
|
||||
@@ -660,7 +660,7 @@ def f(t: TypeVar("U")): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
|
||||
</details>
|
||||
|
||||
## `invalid-metaclass`
|
||||
@@ -692,7 +692,7 @@ class B(metaclass=f): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L677)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L675)
|
||||
</details>
|
||||
|
||||
## `invalid-overload`
|
||||
@@ -740,7 +740,7 @@ def foo(x: int) -> int: ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L704)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
|
||||
</details>
|
||||
|
||||
## `invalid-parameter-default`
|
||||
@@ -765,7 +765,7 @@ def f(a: int = ''): ...
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L747)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L745)
|
||||
</details>
|
||||
|
||||
## `invalid-protocol`
|
||||
@@ -798,7 +798,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L337)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L335)
|
||||
</details>
|
||||
|
||||
## `invalid-raise`
|
||||
@@ -846,7 +846,7 @@ def g():
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L767)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
|
||||
</details>
|
||||
|
||||
## `invalid-return-type`
|
||||
@@ -870,7 +870,7 @@ def func() -> int:
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L428)
|
||||
</details>
|
||||
|
||||
## `invalid-super-argument`
|
||||
@@ -914,7 +914,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L810)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808)
|
||||
</details>
|
||||
|
||||
## `invalid-syntax-in-forward-annotation`
|
||||
@@ -954,7 +954,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L656)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
|
||||
</details>
|
||||
|
||||
## `invalid-type-checking-constant`
|
||||
@@ -983,7 +983,7 @@ TYPE_CHECKING = ''
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L849)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847)
|
||||
</details>
|
||||
|
||||
## `invalid-type-form`
|
||||
@@ -1012,7 +1012,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L873)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
|
||||
</details>
|
||||
|
||||
## `invalid-type-variable-constraints`
|
||||
@@ -1046,7 +1046,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L897)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L895)
|
||||
</details>
|
||||
|
||||
## `missing-argument`
|
||||
@@ -1070,7 +1070,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L926)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924)
|
||||
</details>
|
||||
|
||||
## `no-matching-overload`
|
||||
@@ -1098,7 +1098,7 @@ func("string") # error: [no-matching-overload]
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L943)
|
||||
</details>
|
||||
|
||||
## `non-subscriptable`
|
||||
@@ -1121,7 +1121,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L968)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L966)
|
||||
</details>
|
||||
|
||||
## `not-iterable`
|
||||
@@ -1146,7 +1146,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L986)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L984)
|
||||
</details>
|
||||
|
||||
## `parameter-already-assigned`
|
||||
@@ -1172,7 +1172,7 @@ f(1, x=2) # Error raised here
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035)
|
||||
</details>
|
||||
|
||||
## `raw-string-type-annotation`
|
||||
@@ -1231,7 +1231,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
|
||||
</details>
|
||||
|
||||
## `subclass-of-final-class`
|
||||
@@ -1259,7 +1259,7 @@ class B(A): ... # Error raised here
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1128)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
|
||||
</details>
|
||||
|
||||
## `too-many-positional-arguments`
|
||||
@@ -1285,7 +1285,7 @@ f("foo") # Error raised here
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1173)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
|
||||
</details>
|
||||
|
||||
## `type-assertion-failure`
|
||||
@@ -1312,7 +1312,7 @@ def _(x: int):
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1151)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1149)
|
||||
</details>
|
||||
|
||||
## `unavailable-implicit-super-arguments`
|
||||
@@ -1356,7 +1356,7 @@ class A:
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1194)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1192)
|
||||
</details>
|
||||
|
||||
## `unknown-argument`
|
||||
@@ -1382,7 +1382,7 @@ f(x=1, y=2) # Error raised here
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1249)
|
||||
</details>
|
||||
|
||||
## `unresolved-attribute`
|
||||
@@ -1409,7 +1409,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1270)
|
||||
</details>
|
||||
|
||||
## `unresolved-import`
|
||||
@@ -1433,7 +1433,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1292)
|
||||
</details>
|
||||
|
||||
## `unresolved-reference`
|
||||
@@ -1457,7 +1457,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311)
|
||||
</details>
|
||||
|
||||
## `unsupported-bool-conversion`
|
||||
@@ -1493,7 +1493,7 @@ b1 < b2 < b1 # exception raised here
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1006)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1004)
|
||||
</details>
|
||||
|
||||
## `unsupported-operator`
|
||||
@@ -1520,7 +1520,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1332)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1330)
|
||||
</details>
|
||||
|
||||
## `zero-stepsize-in-slice`
|
||||
@@ -1544,7 +1544,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1354)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352)
|
||||
</details>
|
||||
|
||||
## `invalid-ignore-comment`
|
||||
@@ -1600,7 +1600,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1056)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-implicit-call`
|
||||
@@ -1631,7 +1631,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L111)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
|
||||
</details>
|
||||
|
||||
## `possibly-unbound-import`
|
||||
@@ -1662,7 +1662,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1080)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1078)
|
||||
</details>
|
||||
|
||||
## `redundant-cast`
|
||||
@@ -1688,7 +1688,7 @@ cast(int, f()) # Redundant
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1425)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1423)
|
||||
</details>
|
||||
|
||||
## `undefined-reveal`
|
||||
@@ -1711,7 +1711,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1231)
|
||||
</details>
|
||||
|
||||
## `unknown-rule`
|
||||
@@ -1779,7 +1779,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L489)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L487)
|
||||
</details>
|
||||
|
||||
## `division-by-zero`
|
||||
@@ -1802,7 +1802,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L240)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L238)
|
||||
</details>
|
||||
|
||||
## `possibly-unresolved-reference`
|
||||
@@ -1829,7 +1829,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
|
||||
### Links
|
||||
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106)
|
||||
* [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
|
||||
</details>
|
||||
|
||||
## `unused-ignore-comment`
|
||||
|
||||
@@ -4,7 +4,7 @@ use clap::error::ErrorKind;
|
||||
use clap::{ArgAction, ArgMatches, Error, Parser};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ty_project::combine::Combine;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions};
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf, ValueSource};
|
||||
use ty_python_semantic::lint;
|
||||
|
||||
@@ -107,12 +107,6 @@ pub(crate) struct CheckCommand {
|
||||
#[clap(flatten)]
|
||||
pub(crate) config: ConfigsArg,
|
||||
|
||||
/// The path to a `ty.toml` file to use for configuration.
|
||||
///
|
||||
/// While ty configuration can be included in a `pyproject.toml` file, it is not allowed in this context.
|
||||
#[arg(long, env = "TY_CONFIG_FILE", value_name = "PATH")]
|
||||
pub(crate) config_file: Option<SystemPathBuf>,
|
||||
|
||||
/// The format to use for printing diagnostic messages.
|
||||
#[arg(long)]
|
||||
pub(crate) output_format: Option<OutputFormat>,
|
||||
@@ -190,11 +184,9 @@ impl CheckCommand {
|
||||
.map(|output_format| RangedValue::cli(output_format.into())),
|
||||
error_on_warning: self.error_on_warning,
|
||||
}),
|
||||
src: Some(SrcOptions {
|
||||
respect_ignore_files,
|
||||
..SrcOptions::default()
|
||||
}),
|
||||
rules,
|
||||
respect_ignore_files,
|
||||
..Default::default()
|
||||
};
|
||||
// Merge with options passed in via --config
|
||||
options.combine(self.config.into_options().unwrap_or_default())
|
||||
|
||||
@@ -23,7 +23,7 @@ use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{Db, DummyReporter, Reporter, watch};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
@@ -102,21 +102,13 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(&cwd);
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
let config_file = args
|
||||
.config_file
|
||||
.as_ref()
|
||||
.map(|path| SystemPath::absolute(path, &cwd));
|
||||
|
||||
let mut project_metadata = match &config_file {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
|
||||
None => ProjectMetadata::discover(&project_path, &system)?,
|
||||
};
|
||||
|
||||
let options = args.into_options();
|
||||
project_metadata.apply_options(options.clone());
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
@@ -125,8 +117,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(project_options_overrides);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -187,13 +178,11 @@ struct MainLoop {
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
cli_options: Options,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -201,7 +190,7 @@ impl MainLoop {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
project_options_overrides,
|
||||
cli_options,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
@@ -351,7 +340,7 @@ impl MainLoop {
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.project_options_overrides));
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> {
|
||||
");
|
||||
|
||||
// Test that we can set to false via config file
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
|
||||
case.write_file("ty.toml", "respect-ignore-files = false")?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -104,7 +104,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> {
|
||||
");
|
||||
|
||||
// Ensure CLI takes precedence
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
|
||||
case.write_file("ty.toml", "respect-ignore-files = true")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -242,7 +242,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
|
||||
fn config_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
@@ -308,77 +308,6 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
requires-python = ">=3.8"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
match object():
|
||||
case int():
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | match object():
|
||||
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
|
||||
3 | case int():
|
||||
4 | pass
|
||||
|
|
||||
info: Python 3.8 was assumed when parsing syntax
|
||||
--> pyproject.toml:3:19
|
||||
|
|
||||
2 | [project]
|
||||
3 | requires-python = ">=3.8"
|
||||
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"#);
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | match object():
|
||||
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
3 | case int():
|
||||
4 | pass
|
||||
|
|
||||
info: Python 3.9 was assumed when parsing syntax because it was specified on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paths specified on the CLI are relative to the current working directory and not the project root.
|
||||
///
|
||||
/// We test this by adding an extra search path from the CLI to the libs directory when
|
||||
@@ -1605,7 +1534,7 @@ fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(1)")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
@@ -1615,13 +1544,13 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
|
|
||||
1 | bad-option=true
|
||||
| ^^^^^^^^^^
|
||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`
|
||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `respect-ignore-files`
|
||||
|
||||
|
||||
Usage: ty <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1700,63 +1629,6 @@ fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_override() -> anyhow::Result<()> {
|
||||
// Set `error-on-warning` to true in the configuration file
|
||||
// Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors
|
||||
let case = TestCase::with_files(vec![
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
(
|
||||
"ty-override.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Ensure flag works via CLI arg
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Ensure the flag works via an environment variable
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_db::system::{
|
||||
};
|
||||
use ruff_db::{Db as _, Upcast};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides};
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use ty_project::metadata::pyproject::{PyProject, Tool};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
|
||||
@@ -164,12 +164,8 @@ impl TestCase {
|
||||
Ok(all_events)
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
&mut self,
|
||||
changes: Vec<ChangeEvent>,
|
||||
project_options_overrides: Option<&ProjectOptionsOverrides>,
|
||||
) {
|
||||
self.db.apply_changes(changes, project_options_overrides);
|
||||
fn apply_changes(&mut self, changes: Vec<ChangeEvent>) {
|
||||
self.db.apply_changes(changes, None);
|
||||
}
|
||||
|
||||
fn update_options(&mut self, options: Options) -> anyhow::Result<()> {
|
||||
@@ -184,7 +180,7 @@ impl TestCase {
|
||||
.context("Failed to write configuration")?;
|
||||
|
||||
let changes = self.take_watch_changes(event_for_file("pyproject.toml"));
|
||||
self.apply_changes(changes, None);
|
||||
self.apply_changes(changes);
|
||||
|
||||
if let Some(watcher) = &mut self.watcher {
|
||||
watcher.update(&self.db);
|
||||
@@ -480,7 +476,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let foo = case.system_file(&foo_path).expect("foo.py to exist.");
|
||||
|
||||
@@ -503,7 +499,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
@@ -539,7 +535,7 @@ fn new_non_project_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("black.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&black_path).is_ok());
|
||||
|
||||
@@ -580,7 +576,7 @@ fn new_files_with_explicit_included_paths() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("test2.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let sub_a_file = case.system_file(&sub_a_path).expect("sub/a.py to exist");
|
||||
|
||||
@@ -625,7 +621,7 @@ fn new_file_in_included_out_of_project_directory() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("script2.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let src_a_file = case.system_file(&src_a).unwrap();
|
||||
let outside_b_file = case.system_file(&outside_b_path).unwrap();
|
||||
@@ -652,7 +648,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
|
||||
assert!(!changes.is_empty());
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
case.assert_indexed_project_files([foo]);
|
||||
@@ -675,7 +671,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
case.assert_indexed_project_files([]);
|
||||
@@ -707,7 +703,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
case.assert_indexed_project_files([]);
|
||||
@@ -734,7 +730,7 @@ fn move_file_to_project() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let foo_in_project = case.system_file(&foo_in_project)?;
|
||||
|
||||
@@ -759,7 +755,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("bar.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
|
||||
@@ -800,7 +796,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let init_file = case
|
||||
.system_file(sub_new_path.join("__init__.py"))
|
||||
@@ -857,7 +853,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
@@ -920,7 +916,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
// Linux and windows only emit an event for the newly created root directory, but not for every new component.
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
@@ -993,7 +989,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
@@ -1039,7 +1035,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
|
||||
@@ -1070,7 +1066,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
|
||||
@@ -1217,7 +1213,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("VERSIONS"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
|
||||
|
||||
@@ -1271,7 +1267,7 @@ fn hard_links_in_project() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
|
||||
@@ -1342,7 +1338,7 @@ fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(ChangeEvent::is_changed);
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
|
||||
@@ -1381,7 +1377,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
@@ -1464,7 +1460,7 @@ mod unix {
|
||||
|
||||
let changes = case.take_watch_changes(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_file).as_str(),
|
||||
@@ -1477,7 +1473,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_file).as_str(),
|
||||
@@ -1548,7 +1544,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
|
||||
// if the event is emitted for the "original" or linked path because both paths are watched.
|
||||
@@ -1662,7 +1658,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
@@ -1719,7 +1715,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(ChangeEvent::is_deleted);
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// It should now pick up the outer project.
|
||||
assert_eq!(case.db().project().root(case.db()), case.root_path());
|
||||
@@ -1785,73 +1781,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch(event_for_file("ty.toml"));
|
||||
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
assert!(
|
||||
diagnostics.len() == 1,
|
||||
"Expected exactly one diagnostic but got: {diagnostics:#?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_to_config_file_override() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
std::fs::write(
|
||||
context.join_project_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "test"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
context.join_project_path("foo.py").as_std_path(),
|
||||
"a = 10 / 0",
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
context.join_project_path("ty-override.toml").as_std_path(),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo = case
|
||||
.system_file(case.project_path("foo.py"))
|
||||
.expect("foo.py to exist");
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
assert!(
|
||||
diagnostics.is_empty(),
|
||||
"Expected no diagnostics but got: {diagnostics:#?}"
|
||||
);
|
||||
|
||||
// Enable division-by-zero in the explicitly specified configuration with warning severity
|
||||
update_file(
|
||||
case.project_path("ty-override.toml"),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "warn"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("ty-override.toml"));
|
||||
|
||||
case.apply_changes(
|
||||
changes,
|
||||
Some(&ProjectOptionsOverrides::new(
|
||||
Some(case.project_path("ty-override.toml")),
|
||||
Options::default(),
|
||||
)),
|
||||
);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
@@ -1925,7 +1855,7 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
let changes = case.stop_watch(event_for_file("Lib.py"));
|
||||
case.apply_changes(changes, None);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// Resolving `lib` should now fail but `Lib` should now succeed
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db::{Db, ProjectDatabase};
|
||||
use crate::metadata::options::ProjectOptionsOverrides;
|
||||
use crate::metadata::options::Options;
|
||||
use crate::watch::{ChangeEvent, CreatedKind, DeletedKind};
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use std::collections::BTreeSet;
|
||||
@@ -11,46 +11,20 @@ use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashSet;
|
||||
use ty_python_semantic::Program;
|
||||
|
||||
/// Represents the result of applying changes to the project database.
|
||||
pub struct ChangeResult {
|
||||
project_changed: bool,
|
||||
custom_stdlib_changed: bool,
|
||||
}
|
||||
|
||||
impl ChangeResult {
|
||||
/// Returns `true` if the project structure has changed.
|
||||
pub fn project_changed(&self) -> bool {
|
||||
self.project_changed
|
||||
}
|
||||
|
||||
/// Returns `true` if the custom stdlib's VERSIONS file has changed.
|
||||
pub fn custom_stdlib_changed(&self) -> bool {
|
||||
self.custom_stdlib_changed
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectDatabase {
|
||||
#[tracing::instrument(level = "debug", skip(self, changes, project_options_overrides))]
|
||||
pub fn apply_changes(
|
||||
&mut self,
|
||||
changes: Vec<ChangeEvent>,
|
||||
project_options_overrides: Option<&ProjectOptionsOverrides>,
|
||||
) -> ChangeResult {
|
||||
#[tracing::instrument(level = "debug", skip(self, changes, cli_options))]
|
||||
pub fn apply_changes(&mut self, changes: Vec<ChangeEvent>, cli_options: Option<&Options>) {
|
||||
let mut project = self.project();
|
||||
let project_root = project.root(self).to_path_buf();
|
||||
let config_file_override =
|
||||
project_options_overrides.and_then(|options| options.config_file_override.clone());
|
||||
let options =
|
||||
project_options_overrides.map(|project_options| project_options.options.clone());
|
||||
let program = Program::get(self);
|
||||
let custom_stdlib_versions_path = program
|
||||
.custom_stdlib_search_path(self)
|
||||
.map(|path| path.join("VERSIONS"));
|
||||
|
||||
let mut result = ChangeResult {
|
||||
project_changed: false,
|
||||
custom_stdlib_changed: false,
|
||||
};
|
||||
// Are there structural changes to the project
|
||||
let mut project_changed = false;
|
||||
// Changes to a custom stdlib path's VERSIONS
|
||||
let mut custom_stdlib_change = false;
|
||||
// Paths that were added
|
||||
let mut added_paths = FxHashSet::default();
|
||||
|
||||
@@ -68,26 +42,18 @@ impl ProjectDatabase {
|
||||
tracing::trace!("Handle change: {:?}", change);
|
||||
|
||||
if let Some(path) = change.system_path() {
|
||||
if let Some(config_file) = &config_file_override {
|
||||
if config_file.as_path() == path {
|
||||
result.project_changed = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
path.file_name(),
|
||||
Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml")
|
||||
) {
|
||||
// Changes to ignore files or settings can change the project structure or add/remove files.
|
||||
result.project_changed = true;
|
||||
project_changed = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if Some(path) == custom_stdlib_versions_path.as_deref() {
|
||||
result.custom_stdlib_changed = true;
|
||||
custom_stdlib_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +116,7 @@ impl ProjectDatabase {
|
||||
.as_ref()
|
||||
.is_some_and(|versions_path| versions_path.starts_with(&path))
|
||||
{
|
||||
result.custom_stdlib_changed = true;
|
||||
custom_stdlib_change = true;
|
||||
}
|
||||
|
||||
if project.is_path_included(self, &path) || path == project_root {
|
||||
@@ -164,7 +130,7 @@ impl ProjectDatabase {
|
||||
// We may want to make this more clever in the future, to e.g. iterate over the
|
||||
// indexed files and remove the once that start with the same path, unless
|
||||
// the deleted path is the project configuration.
|
||||
result.project_changed = true;
|
||||
project_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +146,7 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
ChangeEvent::Rescan => {
|
||||
result.project_changed = true;
|
||||
project_changed = true;
|
||||
Files::sync_all(self);
|
||||
sync_recursively.clear();
|
||||
break;
|
||||
@@ -203,15 +169,11 @@ impl ProjectDatabase {
|
||||
last = Some(path);
|
||||
}
|
||||
|
||||
if result.project_changed {
|
||||
let new_project_metadata = match config_file_override {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file, self.system()),
|
||||
None => ProjectMetadata::discover(&project_root, self.system()),
|
||||
};
|
||||
match new_project_metadata {
|
||||
if project_changed {
|
||||
match ProjectMetadata::discover(&project_root, self.system()) {
|
||||
Ok(mut metadata) => {
|
||||
if let Some(cli_options) = options {
|
||||
metadata.apply_options(cli_options);
|
||||
if let Some(cli_options) = cli_options {
|
||||
metadata.apply_cli_options(cli_options.clone());
|
||||
}
|
||||
|
||||
if let Err(error) = metadata.apply_configuration_files(self.system()) {
|
||||
@@ -245,8 +207,8 @@ impl ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else if result.custom_stdlib_changed {
|
||||
return;
|
||||
} else if custom_stdlib_change {
|
||||
let search_paths = project
|
||||
.metadata(self)
|
||||
.to_program_settings(self.system())
|
||||
@@ -276,7 +238,5 @@ impl ProjectDatabase {
|
||||
// implement a `BTreeMap` or similar and only prune the diagnostics from paths that we've
|
||||
// re-scanned (or that were removed etc).
|
||||
project.replace_index_diagnostics(self, diagnostics);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
||||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, create_parse_diagnostic,
|
||||
create_unsupported_syntax_diagnostic,
|
||||
@@ -23,8 +23,8 @@ use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
use ty_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
|
||||
use ty_python_semantic::register_lints;
|
||||
use ty_python_semantic::types::check_types;
|
||||
use ty_python_semantic::{add_inferred_python_version_hint_to_diagnostic, register_lints};
|
||||
|
||||
pub mod combine;
|
||||
|
||||
@@ -460,11 +460,12 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
.map(|error| create_parse_diagnostic(file, error)),
|
||||
);
|
||||
|
||||
diagnostics.extend(parsed.unsupported_syntax_errors().iter().map(|error| {
|
||||
let mut error = create_unsupported_syntax_diagnostic(file, error);
|
||||
add_inferred_python_version_hint_to_diagnostic(db.upcast(), &mut error, "parsing syntax");
|
||||
error
|
||||
}));
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.unsupported_syntax_errors()
|
||||
.iter()
|
||||
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
|
||||
);
|
||||
|
||||
{
|
||||
let db = AssertUnwindSafe(db);
|
||||
|
||||
@@ -48,29 +48,6 @@ impl ProjectMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_config_file(
|
||||
path: SystemPathBuf,
|
||||
system: &dyn System,
|
||||
) -> Result<Self, ProjectMetadataError> {
|
||||
tracing::debug!("Using overridden configuration file at '{path}'");
|
||||
|
||||
let config_file = ConfigurationFile::from_path(path.clone(), system).map_err(|error| {
|
||||
ProjectMetadataError::ConfigurationFileError {
|
||||
source: Box::new(error),
|
||||
path: path.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let options = config_file.into_options();
|
||||
|
||||
Ok(Self {
|
||||
name: Name::new(system.current_directory().file_name().unwrap_or("root")),
|
||||
root: system.current_directory().to_path_buf(),
|
||||
options,
|
||||
extra_configuration_paths: vec![path],
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads a project from a `pyproject.toml` file.
|
||||
pub(crate) fn from_pyproject(
|
||||
pyproject: PyProject,
|
||||
@@ -129,11 +106,11 @@ impl ProjectMetadata {
|
||||
pub fn discover(
|
||||
path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> Result<ProjectMetadata, ProjectMetadataError> {
|
||||
) -> Result<ProjectMetadata, ProjectDiscoveryError> {
|
||||
tracing::debug!("Searching for a project in '{path}'");
|
||||
|
||||
if !system.is_directory(path) {
|
||||
return Err(ProjectMetadataError::NotADirectory(path.to_path_buf()));
|
||||
return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf()));
|
||||
}
|
||||
|
||||
let mut closest_project: Option<ProjectMetadata> = None;
|
||||
@@ -148,7 +125,7 @@ impl ProjectMetadata {
|
||||
) {
|
||||
Ok(pyproject) => Some(pyproject),
|
||||
Err(error) => {
|
||||
return Err(ProjectMetadataError::InvalidPyProject {
|
||||
return Err(ProjectDiscoveryError::InvalidPyProject {
|
||||
path: pyproject_path,
|
||||
source: Box::new(error),
|
||||
});
|
||||
@@ -167,7 +144,7 @@ impl ProjectMetadata {
|
||||
) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return Err(ProjectMetadataError::InvalidTyToml {
|
||||
return Err(ProjectDiscoveryError::InvalidTyToml {
|
||||
path: ty_toml_path,
|
||||
source: Box::new(error),
|
||||
});
|
||||
@@ -194,7 +171,7 @@ impl ProjectMetadata {
|
||||
.and_then(|pyproject| pyproject.project.as_ref()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: pyproject_path,
|
||||
}
|
||||
@@ -208,7 +185,7 @@ impl ProjectMetadata {
|
||||
let metadata =
|
||||
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
|
||||
.map_err(
|
||||
|err| ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: pyproject_path,
|
||||
},
|
||||
@@ -272,7 +249,7 @@ impl ProjectMetadata {
|
||||
}
|
||||
|
||||
/// Combine the project options with the CLI options where the CLI options take precedence.
|
||||
pub fn apply_options(&mut self, options: Options) {
|
||||
pub fn apply_cli_options(&mut self, options: Options) {
|
||||
self.options = options.combine(std::mem::take(&mut self.options));
|
||||
}
|
||||
|
||||
@@ -305,7 +282,7 @@ impl ProjectMetadata {
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProjectMetadataError {
|
||||
pub enum ProjectDiscoveryError {
|
||||
#[error("project path '{0}' is not a directory")]
|
||||
NotADirectory(SystemPathBuf),
|
||||
|
||||
@@ -326,12 +303,6 @@ pub enum ProjectMetadataError {
|
||||
source: ResolveRequiresPythonError,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
|
||||
#[error("Error loading configuration file at {path}: {source}")]
|
||||
ConfigurationFileError {
|
||||
source: Box<ConfigurationFileError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -343,7 +314,7 @@ mod tests {
|
||||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::{ProjectMetadata, ProjectMetadataError};
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
#[test]
|
||||
fn project_without_pyproject() -> anyhow::Result<()> {
|
||||
@@ -1105,7 +1076,7 @@ expected `.`, `]`
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_error_eq(error: &ProjectMetadataError, message: &str) {
|
||||
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
|
||||
assert_eq!(error.to_string().replace('\\', "/"), message);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,25 +14,6 @@ pub(crate) struct ConfigurationFile {
|
||||
}
|
||||
|
||||
impl ConfigurationFile {
|
||||
pub(crate) fn from_path(
|
||||
path: SystemPathBuf,
|
||||
system: &dyn System,
|
||||
) -> Result<Self, ConfigurationFileError> {
|
||||
let ty_toml_str = system.read_to_string(&path).map_err(|source| {
|
||||
ConfigurationFileError::FileReadError {
|
||||
source,
|
||||
path: path.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match Options::from_toml_str(&ty_toml_str, ValueSource::File(Arc::new(path.clone()))) {
|
||||
Ok(options) => Ok(Self { path, options }),
|
||||
Err(error) => Err(ConfigurationFileError::InvalidTyToml {
|
||||
source: Box::new(error),
|
||||
path,
|
||||
}),
|
||||
}
|
||||
}
|
||||
/// Loads the user-level configuration file if it exists.
|
||||
///
|
||||
/// Returns `None` if the file does not exist or if the concept of user-level configurations
|
||||
@@ -85,10 +66,4 @@ pub enum ConfigurationFileError {
|
||||
source: Box<TyTomlError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
#[error("Failed to read `{path}`: {source}")]
|
||||
FileReadError {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::Db;
|
||||
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::{Combine, OptionsMetadata};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -57,6 +57,19 @@ pub struct Options {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub terminal: Option<TerminalOptions>,
|
||||
|
||||
/// Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
/// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
/// Enabled by default.
|
||||
#[option(
|
||||
default = r#"true"#,
|
||||
value_type = r#"bool"#,
|
||||
example = r#"
|
||||
respect-ignore-files = false
|
||||
"#
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub respect_ignore_files: Option<bool>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@@ -203,7 +216,7 @@ impl Options {
|
||||
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
|
||||
let (rules, diagnostics) = self.to_rule_selection(db);
|
||||
|
||||
let mut settings = Settings::new(rules, self.src.as_ref());
|
||||
let mut settings = Settings::new(rules, self.respect_ignore_files);
|
||||
|
||||
if let Some(terminal) = self.terminal.as_ref() {
|
||||
settings.set_terminal(TerminalSettings {
|
||||
@@ -408,19 +421,6 @@ pub struct SrcOptions {
|
||||
"#
|
||||
)]
|
||||
pub root: Option<RelativePathBuf>,
|
||||
|
||||
/// Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
/// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
/// Enabled by default.
|
||||
#[option(
|
||||
default = r#"true"#,
|
||||
value_type = r#"bool"#,
|
||||
example = r#"
|
||||
respect-ignore-files = false
|
||||
"#
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub respect_ignore_files: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
@@ -575,20 +575,3 @@ impl OptionDiagnostic {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a wrapper for options that actually get loaded from configuration files
|
||||
/// and the CLI, which also includes a `config_file_override` option that overrides
|
||||
/// default configuration discovery with an explicitly-provided path to a configuration file
|
||||
pub struct ProjectOptionsOverrides {
|
||||
pub config_file_override: Option<SystemPathBuf>,
|
||||
pub options: Options,
|
||||
}
|
||||
|
||||
impl ProjectOptionsOverrides {
|
||||
pub fn new(config_file_override: Option<SystemPathBuf>, options: Options) -> Self {
|
||||
Self {
|
||||
config_file_override,
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::metadata::options::SrcOptions;
|
||||
use ruff_db::diagnostic::DiagnosticFormat;
|
||||
use ty_python_semantic::lint::RuleSelection;
|
||||
|
||||
@@ -27,15 +26,11 @@ pub struct Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new(rules: RuleSelection, src_options: Option<&SrcOptions>) -> Self {
|
||||
let respect_ignore_files = src_options
|
||||
.and_then(|src| src.respect_ignore_files)
|
||||
.unwrap_or(true);
|
||||
|
||||
pub fn new(rules: RuleSelection, respect_ignore_files: Option<bool>) -> Self {
|
||||
Self {
|
||||
rules: Arc::new(rules),
|
||||
terminal: TerminalSettings::default(),
|
||||
respect_ignore_files,
|
||||
respect_ignore_files: respect_ignore_files.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2109,6 +2109,52 @@ reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
```
|
||||
|
||||
## Errors for unresolved and invalid attribute assignments
|
||||
|
||||
```py
|
||||
class C:
|
||||
class_attr: int = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.instance_attr: int = 1
|
||||
|
||||
c = C()
|
||||
|
||||
C.class_attr = 2
|
||||
c.class_attr = 2
|
||||
c.instance_attr = 2
|
||||
|
||||
C.class_attr = "invalid" # error: [invalid-assignment]
|
||||
c.class_attr = "invalid" # error: [invalid-assignment]
|
||||
c.instance_attr = "invalid" # error: [invalid-assignment]
|
||||
|
||||
# TODO: The following attribute assignments are flagged by mypy/pyright (Type can
|
||||
# not be declared in assignment to non-self attribute). We currently do not emit
|
||||
# any errors and also don't validate the type of the annotation in any form. This
|
||||
# should probably be changed.
|
||||
C.class_attr: None = 2
|
||||
c.class_attr: None = 2
|
||||
c.instance_attr: None = 2
|
||||
|
||||
# TODO: Similar here. We do report `invalid-assignment` errors, but only because
|
||||
# the value type does not match.
|
||||
C.class_attr: str = "invalid" # error: [invalid-assignment]
|
||||
c.class_attr: str = "invalid" # error: [invalid-assignment]
|
||||
c.instance_attr: str = "invalid" # error: [invalid-assignment]
|
||||
|
||||
# For non-existent attributes, we emit `unresolved-attribute` errors:
|
||||
C.non_existent = 2 # error: [unresolved-attribute]
|
||||
c.non_existent = 2 # error: [unresolved-attribute]
|
||||
|
||||
# TODO: Similar to above, these should either be forbidden or validated.
|
||||
#
|
||||
# Make sure that we emit `unresolved-attribute` errors, at least:
|
||||
C.non_existent: int = 2 # error: [unresolved-attribute]
|
||||
c.non_existent: int = 2 # error: [unresolved-attribute]
|
||||
C.non_existent: None = 2 # error: [unresolved-attribute]
|
||||
c.non_existent: None = 2 # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
||||
@@ -59,8 +59,6 @@ ClassWithNormalDunder[0]
|
||||
|
||||
## Operating on instances
|
||||
|
||||
### Attaching dunder methods to instances in methods
|
||||
|
||||
When invoking a dunder method on an instance of a class, it is looked up on the class:
|
||||
|
||||
```py
|
||||
@@ -118,40 +116,6 @@ def _(flag: bool):
|
||||
reveal_type(this_fails[0]) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
### Dunder methods as class-level annotations with no value
|
||||
|
||||
Class-level annotations with no value assigned are considered instance-only, and aren't available as
|
||||
dunder methods:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class C:
|
||||
__call__: Callable[..., None]
|
||||
|
||||
# error: [call-non-callable]
|
||||
C()()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
_: Callable[..., None] = C()
|
||||
```
|
||||
|
||||
And of course the same is true if we have only an implicit assignment inside a method:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.__call__ = lambda *a, **kw: None
|
||||
|
||||
# error: [call-non-callable]
|
||||
C()()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
_: Callable[..., None] = C()
|
||||
```
|
||||
|
||||
## When the dunder is not a method
|
||||
|
||||
A dunder can also be a non-method callable:
|
||||
@@ -275,3 +239,37 @@ def _(flag: bool):
|
||||
# error: [possibly-unbound-implicit-call]
|
||||
reveal_type(c[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Dunder methods cannot be looked up on instances
|
||||
|
||||
Class-level annotations with no value assigned are considered instance-only, and aren't available as
|
||||
dunder methods:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class C:
|
||||
__call__: Callable[..., None]
|
||||
|
||||
# error: [call-non-callable]
|
||||
C()()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
_: Callable[..., None] = C()
|
||||
```
|
||||
|
||||
And of course the same is true if we have only an implicit assignment inside a method:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.__call__ = lambda *a, **kw: None
|
||||
|
||||
# error: [call-non-callable]
|
||||
C()()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
_: Callable[..., None] = C()
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ class RepeatedTypevar(Generic[T, T]): ...
|
||||
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
|
||||
|
||||
```py
|
||||
# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `Generic`"
|
||||
# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `typing.Generic`"
|
||||
class GenericOfType(Generic[int]): ...
|
||||
```
|
||||
|
||||
|
||||
@@ -67,41 +67,6 @@ T = TypeVar("T")
|
||||
|
||||
# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables"
|
||||
class BothGenericSyntaxes[U](Generic[T]): ...
|
||||
|
||||
reveal_type(BothGenericSyntaxes.__mro__) # revealed: tuple[<class 'BothGenericSyntaxes[Unknown]'>, Unknown, <class 'object'>]
|
||||
|
||||
# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables"
|
||||
# error: [invalid-base] "Cannot inherit from plain `Generic`"
|
||||
class DoublyInvalid[T](Generic): ...
|
||||
|
||||
reveal_type(DoublyInvalid.__mro__) # revealed: tuple[<class 'DoublyInvalid[Unknown]'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
Generic classes implicitly inherit from `Generic`:
|
||||
|
||||
```py
|
||||
class Foo[T]: ...
|
||||
|
||||
# revealed: tuple[<class 'Foo[Unknown]'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Foo.__mro__)
|
||||
# revealed: tuple[<class 'Foo[int]'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Foo[int].__mro__)
|
||||
|
||||
class A: ...
|
||||
class Bar[T](A): ...
|
||||
|
||||
# revealed: tuple[<class 'Bar[Unknown]'>, <class 'A'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Bar.__mro__)
|
||||
# revealed: tuple[<class 'Bar[int]'>, <class 'A'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Bar[int].__mro__)
|
||||
|
||||
class B: ...
|
||||
class Baz[T](A, B): ...
|
||||
|
||||
# revealed: tuple[<class 'Baz[Unknown]'>, <class 'A'>, <class 'B'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Baz.__mro__)
|
||||
# revealed: tuple[<class 'Baz[int]'>, <class 'A'>, <class 'B'>, typing.Generic, <class 'object'>]
|
||||
reveal_type(Baz[int].__mro__)
|
||||
```
|
||||
|
||||
## Specializing generic classes explicitly
|
||||
|
||||
@@ -644,14 +644,14 @@ reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>
|
||||
|
||||
class D(D.a):
|
||||
a: D
|
||||
reveal_type(D.__class__) # revealed: <class 'type'>
|
||||
#reveal_type(D.__class__) # revealed: <class 'type'>
|
||||
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, Unknown, <class 'object'>]
|
||||
|
||||
class E[T](E.a): ...
|
||||
reveal_type(E.__class__) # revealed: <class 'type'>
|
||||
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, typing.Generic, <class 'object'>]
|
||||
#reveal_type(E.__class__) # revealed: <class 'type'>
|
||||
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, <class 'object'>]
|
||||
|
||||
class F[T](F(), F): ... # error: [cyclic-class-definition]
|
||||
reveal_type(F.__class__) # revealed: type[Unknown]
|
||||
#reveal_type(F.__class__) # revealed: <class 'type'>
|
||||
reveal_type(F.__mro__) # revealed: tuple[<class 'F[Unknown]'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -58,13 +58,9 @@ class Bar1(Protocol[T], Generic[T]):
|
||||
class Bar2[T](Protocol):
|
||||
x: T
|
||||
|
||||
# error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables"
|
||||
# error: [invalid-generic-class] "Cannot both inherit from subscripted `typing.Protocol` and use PEP 695 type variables"
|
||||
class Bar3[T](Protocol[T]):
|
||||
x: T
|
||||
|
||||
# Note that this class definition *will* actually succeed at runtime,
|
||||
# unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]`
|
||||
reveal_type(Bar3.__mro__) # revealed: tuple[<class 'Bar3[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
```
|
||||
|
||||
It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: sync.md - With statements - Accidental use of non-async `with`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class Manager:
|
||||
2 | async def __aenter__(self): ...
|
||||
3 | async def __aexit__(self, *args): ...
|
||||
4 |
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
6 | with Manager():
|
||||
7 | ...
|
||||
8 | class Manager:
|
||||
9 | async def __aenter__(self): ...
|
||||
10 | async def __aexit__(self, typ: str, exc, traceback): ...
|
||||
11 |
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
13 | with Manager():
|
||||
14 | ...
|
||||
15 | class Manager:
|
||||
16 | async def __aenter__(self, wrong_extra_arg): ...
|
||||
17 | async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ...
|
||||
18 |
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
20 | with Manager():
|
||||
21 | ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:6:6
|
||||
|
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and...
|
||||
6 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
7 | ...
|
||||
8 | class Manager:
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:13:6
|
||||
|
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
13 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
14 | ...
|
||||
15 | class Manager:
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:20:6
|
||||
|
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
20 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
21 | ...
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
||||
@@ -149,45 +149,3 @@ context_expr = Manager()
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Accidental use of non-async `with`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
If a synchronous `with` statement is used on a type with `__aenter__` and `__aexit__`, we show a
|
||||
diagnostic hint that the user might have intended to use `asnyc with` instead.
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self): ...
|
||||
async def __aexit__(self, *args): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
The sub-diagnostic is also provided if the signatures of `__aenter__` and `__aexit__` do not match
|
||||
the expected signatures for a context manager:
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self): ...
|
||||
async def __aexit__(self, typ: str, exc, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
Similarly, we also show the hint if the functions have the wrong number of arguments:
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self, wrong_extra_arg): ...
|
||||
async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
Tanjun # hangs
|
||||
antidote # hangs / slow
|
||||
artigraph # cycle panics (value_type_)
|
||||
arviz # too many iterations on versions of arviz newer than https://github.com/arviz-devs/arviz/commit/3205b82bb4d6097c31f7334d7ac51a6de37002d0
|
||||
core # cycle panics (value_type_)
|
||||
cpython # access to field whilst being initialized, too many cycle iterations
|
||||
discord.py # some kind of hang, only when multi-threaded?
|
||||
|
||||
@@ -12,6 +12,7 @@ alerta
|
||||
altair
|
||||
anyio
|
||||
apprise
|
||||
arviz
|
||||
async-utils
|
||||
asynq
|
||||
attrs
|
||||
|
||||
@@ -14,7 +14,6 @@ pub use program::{
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use semantic_model::{HasType, SemanticModel};
|
||||
pub use site_packages::SysPrefixPathOrigin;
|
||||
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod db;
|
||||
|
||||
@@ -50,7 +50,6 @@ use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
||||
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
|
||||
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;
|
||||
@@ -1026,10 +1025,34 @@ impl<'db> Type<'db> {
|
||||
Type::BoundSuper(bound_super) => Type::BoundSuper(bound_super.normalized(db)),
|
||||
Type::GenericAlias(generic) => Type::GenericAlias(generic.normalized(db)),
|
||||
Type::SubclassOf(subclass_of) => Type::SubclassOf(subclass_of.normalized(db)),
|
||||
Type::TypeVar(typevar) => Type::TypeVar(typevar.normalized(db)),
|
||||
Type::KnownInstance(known_instance) => {
|
||||
Type::KnownInstance(known_instance.normalized(db))
|
||||
}
|
||||
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
typevar.name(db).clone(),
|
||||
typevar.definition(db),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound.normalized(db))),
|
||||
typevar.variance(db),
|
||||
typevar.default_ty(db),
|
||||
typevar.kind(db),
|
||||
))
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(union)) => {
|
||||
Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
typevar.name(db).clone(),
|
||||
typevar.definition(db),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(union.normalized(db))),
|
||||
typevar.variance(db),
|
||||
typevar.default_ty(db),
|
||||
typevar.kind(db),
|
||||
))
|
||||
}
|
||||
None => self,
|
||||
},
|
||||
Type::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
@@ -3413,9 +3436,6 @@ impl<'db> Type<'db> {
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysTruthy => Truthiness::AlwaysTrue,
|
||||
|
||||
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
|
||||
@@ -3451,6 +3471,10 @@ impl<'db> Type<'db> {
|
||||
|
||||
Type::ProtocolInstance(_) => try_dunder_bool()?,
|
||||
|
||||
Type::KnownInstance(known_instance) => known_instance.bool(),
|
||||
|
||||
Type::PropertyInstance(_) => Truthiness::AlwaysTrue,
|
||||
|
||||
Type::Union(union) => try_union(*union)?,
|
||||
|
||||
Type::Intersection(_) => {
|
||||
@@ -3463,6 +3487,7 @@ impl<'db> Type<'db> {
|
||||
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
|
||||
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
|
||||
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
|
||||
Type::BoundSuper(_) => Truthiness::AlwaysTrue,
|
||||
};
|
||||
|
||||
Ok(truthiness)
|
||||
@@ -6108,31 +6133,12 @@ impl<'db> ContextManagerError<'db> {
|
||||
} => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"),
|
||||
};
|
||||
|
||||
let mut diag = builder.into_diagnostic(
|
||||
builder.into_diagnostic(
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}",
|
||||
context_expression = context_expression_type.display(db)
|
||||
),
|
||||
);
|
||||
|
||||
// If `__aenter__` and `__aexit__` are available, the user may have intended to use `async with` instead of `with`:
|
||||
if let (
|
||||
Ok(_) | Err(CallDunderError::CallError(..)),
|
||||
Ok(_) | Err(CallDunderError::CallError(..)),
|
||||
) = (
|
||||
context_expression_type.try_call_dunder(db, "__aenter__", CallArgumentTypes::none()),
|
||||
context_expression_type.try_call_dunder(
|
||||
db,
|
||||
"__aexit__",
|
||||
CallArgumentTypes::positional([Type::unknown(), Type::unknown(), Type::unknown()]),
|
||||
),
|
||||
) {
|
||||
diag.info(format_args!(
|
||||
"Objects of type `{context_expression}` can be used as async context managers",
|
||||
context_expression = context_expression_type.display(db)
|
||||
));
|
||||
diag.info("Consider using `async with` here");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -655,26 +655,6 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::HasAttr) => {
|
||||
if let [Some(obj), Some(Type::StringLiteral(attr_name))] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
match obj.member(db, attr_name.value(db)).symbol {
|
||||
Symbol::Type(_, Boundness::Bound) => {
|
||||
overload.set_return_type(Type::BooleanLiteral(true));
|
||||
}
|
||||
Symbol::Type(_, Boundness::PossiblyUnbound) => {
|
||||
// Fall back to bool (from typeshed)
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
// Returning `Literal[False]` here seems potentially
|
||||
// dangerous. The attribute could have been added
|
||||
// dynamically, so fall back to `bool` here to be safe.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsProtocol) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
|
||||
@@ -223,11 +223,8 @@ impl<'db> ClassType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Self::NonGeneric(class) => class.has_pep_695_type_params(db),
|
||||
Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db),
|
||||
}
|
||||
pub(super) const fn is_generic(self) -> bool {
|
||||
matches!(self, Self::Generic(_))
|
||||
}
|
||||
|
||||
/// Returns the class literal and specialization for this class. For a non-generic class, this
|
||||
@@ -576,10 +573,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
.or_else(|| self.inherited_legacy_generic_context(db))
|
||||
}
|
||||
|
||||
pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
|
||||
self.pep695_generic_context(db).is_some()
|
||||
}
|
||||
|
||||
#[salsa::tracked(cycle_fn=pep695_generic_context_cycle_recover, cycle_initial=pep695_generic_context_cycle_initial)]
|
||||
pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
|
||||
let scope = self.body_scope(db);
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use super::call::CallErrorKind;
|
||||
use super::context::InferContext;
|
||||
use super::mro::DuplicateBaseError;
|
||||
use super::{
|
||||
CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass,
|
||||
add_inferred_python_version_hint_to_diagnostic,
|
||||
};
|
||||
use super::{CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass};
|
||||
use crate::db::Db;
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
@@ -16,8 +12,10 @@ use crate::types::string_annotation::{
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{KnownFunction, KnownInstanceType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::{Program, PythonVersionWithSource, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_stdlib::builtins::version_builtin_was_added;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -1764,6 +1762,44 @@ pub(super) fn report_possibly_unbound_attribute(
|
||||
));
|
||||
}
|
||||
|
||||
pub(super) fn add_inferred_python_version_hint(db: &dyn Db, mut diagnostic: LintDiagnosticGuard) {
|
||||
let program = Program::get(db);
|
||||
let PythonVersionWithSource { version, source } = program.python_version_with_source(db);
|
||||
|
||||
match source {
|
||||
crate::PythonVersionSource::Cli => {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when resolving types because it was specified on the command line",
|
||||
));
|
||||
}
|
||||
crate::PythonVersionSource::File(path, range) => {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &**path) {
|
||||
let mut sub_diagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format_args!("Python {version} was assumed when resolving types"),
|
||||
);
|
||||
sub_diagnostic.annotate(
|
||||
Annotation::primary(Span::from(file).with_optional_range(*range)).message(
|
||||
format_args!("Python {version} assumed due to this configuration setting"),
|
||||
),
|
||||
);
|
||||
diagnostic.sub(sub_diagnostic);
|
||||
} else {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when resolving types because of your configuration file(s)",
|
||||
));
|
||||
}
|
||||
}
|
||||
crate::PythonVersionSource::Default => {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when resolving types \
|
||||
because it is the newest Python version supported by ty, \
|
||||
and neither a command-line argument nor a configuration setting was provided",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
|
||||
let Some(builder) = context.report_lint(&UNRESOLVED_REFERENCE, expr_name_node) else {
|
||||
return;
|
||||
@@ -1775,11 +1811,7 @@ pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node
|
||||
diagnostic.info(format_args!(
|
||||
"`{id}` was added as a builtin in Python 3.{version_added_to_builtins}"
|
||||
));
|
||||
add_inferred_python_version_hint_to_diagnostic(
|
||||
context.db(),
|
||||
&mut diagnostic,
|
||||
"resolving types",
|
||||
);
|
||||
add_inferred_python_version_hint(context.db(), diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::{Db, FxOrderSet};
|
||||
pub struct GenericContext<'db> {
|
||||
#[returns(ref)]
|
||||
pub(crate) variables: FxOrderSet<TypeVarInstance<'db>>,
|
||||
pub(crate) origin: GenericContextOrigin,
|
||||
}
|
||||
|
||||
impl<'db> GenericContext<'db> {
|
||||
@@ -40,7 +41,7 @@ impl<'db> GenericContext<'db> {
|
||||
.iter()
|
||||
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
|
||||
.collect();
|
||||
Self::new(db, variables)
|
||||
Self::new(db, variables, GenericContextOrigin::TypeParameterList)
|
||||
}
|
||||
|
||||
fn variable_from_type_param(
|
||||
@@ -86,7 +87,11 @@ impl<'db> GenericContext<'db> {
|
||||
if variables.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::new(db, variables))
|
||||
Some(Self::new(
|
||||
db,
|
||||
variables,
|
||||
GenericContextOrigin::LegacyGenericFunction,
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates a generic context from the legacy `TypeVar`s that appear in class's base class
|
||||
@@ -102,7 +107,7 @@ impl<'db> GenericContext<'db> {
|
||||
if variables.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::new(db, variables))
|
||||
Some(Self::new(db, variables, GenericContextOrigin::Inherited))
|
||||
}
|
||||
|
||||
pub(crate) fn len(self, db: &'db dyn Db) -> usize {
|
||||
@@ -239,21 +244,46 @@ impl<'db> GenericContext<'db> {
|
||||
.iter()
|
||||
.map(|ty| ty.normalized(db))
|
||||
.collect();
|
||||
Self::new(db, variables)
|
||||
Self::new(db, variables, self.origin(db))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum LegacyGenericBase {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum GenericContextOrigin {
|
||||
LegacyBase(LegacyGenericBase),
|
||||
Inherited,
|
||||
LegacyGenericFunction,
|
||||
TypeParameterList,
|
||||
}
|
||||
|
||||
impl GenericContextOrigin {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::LegacyBase(base) => base.as_str(),
|
||||
Self::Inherited => "inherited",
|
||||
Self::LegacyGenericFunction => "legacy generic function",
|
||||
Self::TypeParameterList => "type parameter list",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GenericContextOrigin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum LegacyGenericBase {
|
||||
Generic,
|
||||
Protocol,
|
||||
}
|
||||
|
||||
impl LegacyGenericBase {
|
||||
const fn as_str(self) -> &'static str {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Generic => "Generic",
|
||||
Self::Protocol => "Protocol",
|
||||
Self::Generic => "`typing.Generic`",
|
||||
Self::Protocol => "subscripted `typing.Protocol`",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,6 +294,12 @@ impl std::fmt::Display for LegacyGenericBase {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LegacyGenericBase> for GenericContextOrigin {
|
||||
fn from(base: LegacyGenericBase) -> Self {
|
||||
Self::LegacyBase(base)
|
||||
}
|
||||
}
|
||||
|
||||
/// An assignment of a specific type to each type variable in a generic scope.
|
||||
///
|
||||
/// TODO: Handle nested specializations better, with actual parent links to the specialization of
|
||||
|
||||
@@ -108,15 +108,13 @@ use super::diagnostic::{
|
||||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
report_unresolved_reference,
|
||||
};
|
||||
use super::generics::LegacyGenericBase;
|
||||
use super::generics::{GenericContextOrigin, LegacyGenericBase};
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
use super::subclass_of::SubclassOfInner;
|
||||
use super::{
|
||||
BoundSuperError, BoundSuperType, ClassBase, add_inferred_python_version_hint_to_diagnostic,
|
||||
};
|
||||
use super::{BoundSuperError, BoundSuperType, ClassBase};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
@@ -858,25 +856,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Note that unlike several of the other errors caught in this function,
|
||||
// this does not lead to the class creation failing at runtime,
|
||||
// but it is semantically invalid.
|
||||
Type::KnownInstance(KnownInstanceType::Protocol(Some(_))) => {
|
||||
if class_node.type_params.is_none() {
|
||||
continue;
|
||||
}
|
||||
let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_GENERIC_CLASS, &class_node.bases()[i])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
builder.into_diagnostic(
|
||||
"Cannot both inherit from subscripted `Protocol` \
|
||||
and use PEP 695 type variables",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Type::ClassLiteral(class) => class,
|
||||
// dynamic/unknown bases are never `@final`
|
||||
_ => continue,
|
||||
@@ -938,7 +917,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) \
|
||||
for class `{}` with bases list `[{}]`",
|
||||
for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db()),
|
||||
bases_list
|
||||
.iter()
|
||||
@@ -947,16 +926,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
));
|
||||
}
|
||||
}
|
||||
MroErrorKind::Pep695ClassWithGenericInheritance => {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"Cannot both inherit from `typing.Generic` \
|
||||
and use PEP 695 type variables",
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::InheritanceCycle => {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
@@ -1053,6 +1022,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// (5) Check that a generic class does not have invalid or conflicting generic
|
||||
// contexts.
|
||||
if class.pep695_generic_context(self.db()).is_some() {
|
||||
if let Some(legacy_context) = class.legacy_generic_context(self.db()) {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot both inherit from {} and use PEP 695 type variables",
|
||||
legacy_context.origin(self.db())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(legacy), Some(inherited)) = (
|
||||
class.legacy_generic_context(self.db()),
|
||||
class.inherited_legacy_generic_context(self.db()),
|
||||
@@ -3565,11 +3549,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = assignment;
|
||||
let annotated =
|
||||
self.infer_annotation_expression(annotation, DeferredExpressionState::None);
|
||||
self.infer_optional_expression(value.as_deref());
|
||||
|
||||
// If we have an annotated assignment like `self.attr: int = 1`, we still need to
|
||||
// do type inference on the `self.attr` target to get types for all sub-expressions.
|
||||
self.infer_expression(target);
|
||||
if let Some(value) = value {
|
||||
let value_ty = self.infer_expression(value);
|
||||
self.infer_target(target, value, |_builder, _value_expr| value_ty);
|
||||
} else {
|
||||
self.infer_expression(target);
|
||||
}
|
||||
|
||||
// But here we explicitly overwrite the type for the overall `self.attr` node with
|
||||
// the annotated type. We do no use `store_expression_type` here, because it checks
|
||||
@@ -6031,7 +6019,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
diag.info(
|
||||
"Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later",
|
||||
);
|
||||
add_inferred_python_version_hint_to_diagnostic(db, &mut diag, "resolving types");
|
||||
diagnostic::add_inferred_python_version_hint(db, diag);
|
||||
}
|
||||
}
|
||||
Type::unknown()
|
||||
@@ -7644,7 +7632,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`{}` is not a valid argument to `{origin}`",
|
||||
"`{}` is not a valid argument to {origin}",
|
||||
typevar.display(self.db()),
|
||||
));
|
||||
}
|
||||
@@ -7652,7 +7640,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
typevars.map(|typevars| GenericContext::new(self.db(), typevars))
|
||||
typevars.map(|typevars| {
|
||||
GenericContext::new(self.db(), typevars, GenericContextOrigin::from(origin))
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> {
|
||||
|
||||
@@ -11,21 +11,14 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::generics::GenericContext;
|
||||
use super::{ClassType, Type, TypeAliasType, TypeVarInstance, class::KnownClass};
|
||||
use super::{ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance, class::KnownClass};
|
||||
use crate::db::Db;
|
||||
use crate::module_resolver::{KnownModule, file_to_module};
|
||||
use ruff_db::files::File;
|
||||
|
||||
/// Enumeration of specific runtime symbols that are special enough
|
||||
/// that they can each be considered to inhabit a unique type.
|
||||
///
|
||||
/// # Ordering
|
||||
///
|
||||
/// Ordering between variants is stable and should be the same between runs.
|
||||
/// Ordering within variants (for variants that wrap associate data)
|
||||
/// is based on the known-instance's salsa-assigned id and not on its values.
|
||||
/// The id may change between runs, or when the type var instance was garbage collected and recreated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub enum KnownInstanceType<'db> {
|
||||
/// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`)
|
||||
Annotated,
|
||||
@@ -108,6 +101,58 @@ pub enum KnownInstanceType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> KnownInstanceType<'db> {
|
||||
/// Evaluate the known instance in boolean context
|
||||
pub(crate) const fn bool(self) -> Truthiness {
|
||||
match self {
|
||||
Self::Annotated
|
||||
| Self::Literal
|
||||
| Self::LiteralString
|
||||
| Self::Optional
|
||||
// This is a legacy `TypeVar` _outside_ of any generic class or function, so it's
|
||||
// AlwaysTrue. The truthiness of a typevar inside of a generic class or function
|
||||
// depends on its bounds and constraints; but that's represented by `Type::TypeVar` and
|
||||
// handled in elsewhere.
|
||||
| Self::TypeVar(_)
|
||||
| Self::Union
|
||||
| Self::NoReturn
|
||||
| Self::Never
|
||||
| Self::Tuple
|
||||
| Self::Type
|
||||
| Self::TypingSelf
|
||||
| Self::Final
|
||||
| Self::ClassVar
|
||||
| Self::Callable
|
||||
| Self::Concatenate
|
||||
| Self::Unpack
|
||||
| Self::Required
|
||||
| Self::NotRequired
|
||||
| Self::TypeAlias
|
||||
| Self::TypeGuard
|
||||
| Self::TypedDict
|
||||
| Self::TypeIs
|
||||
| Self::List
|
||||
| Self::Dict
|
||||
| Self::DefaultDict
|
||||
| Self::Set
|
||||
| Self::FrozenSet
|
||||
| Self::Counter
|
||||
| Self::Deque
|
||||
| Self::ChainMap
|
||||
| Self::OrderedDict
|
||||
| Self::Protocol(_)
|
||||
| Self::Generic(_)
|
||||
| Self::ReadOnly
|
||||
| Self::TypeAliasType(_)
|
||||
| Self::Unknown
|
||||
| Self::AlwaysTruthy
|
||||
| Self::AlwaysFalsy
|
||||
| Self::Not
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
match self {
|
||||
Self::Annotated
|
||||
|
||||
@@ -67,41 +67,10 @@ impl<'db> Mro<'db> {
|
||||
fn of_class_impl(
|
||||
db: &'db dyn Db,
|
||||
class: ClassType<'db>,
|
||||
original_bases: &[Type<'db>],
|
||||
bases: &[Type<'db>],
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Self, MroErrorKind<'db>> {
|
||||
/// Possibly add `Generic` to the resolved bases list.
|
||||
///
|
||||
/// This function is called in two cases:
|
||||
/// - If we encounter a subscripted `Generic` in the original bases list
|
||||
/// (`Generic[T]` or similar)
|
||||
/// - If the class has PEP-695 type parameters,
|
||||
/// `Generic` is [implicitly appended] to the bases list at runtime
|
||||
///
|
||||
/// Whether or not `Generic` is added to the bases list depends on:
|
||||
/// - Whether `Protocol` is present in the original bases list
|
||||
/// - Whether any of the bases yet to be visited in the original bases list
|
||||
/// is a generic alias (which would therefore have `Generic` in its MRO)
|
||||
///
|
||||
/// This function emulates the behavior of `typing._GenericAlias.__mro_entries__` at
|
||||
/// <https://github.com/python/cpython/blob/ad42dc1909bdf8ec775b63fb22ed48ff42797a17/Lib/typing.py#L1487-L1500>.
|
||||
///
|
||||
/// [implicitly inherits]: https://docs.python.org/3/reference/compound_stmts.html#generic-classes
|
||||
fn maybe_add_generic<'db>(
|
||||
resolved_bases: &mut Vec<ClassBase<'db>>,
|
||||
original_bases: &[Type<'db>],
|
||||
remaining_bases: &[Type<'db>],
|
||||
) {
|
||||
if original_bases.contains(&Type::KnownInstance(KnownInstanceType::Protocol(None))) {
|
||||
return;
|
||||
}
|
||||
if remaining_bases.iter().any(Type::is_generic_alias) {
|
||||
return;
|
||||
}
|
||||
resolved_bases.push(ClassBase::Generic);
|
||||
}
|
||||
|
||||
match original_bases {
|
||||
match bases {
|
||||
// `builtins.object` is the special case:
|
||||
// the only class in Python that has an MRO with length <2
|
||||
[] if class.is_object(db) => Ok(Self::from([
|
||||
@@ -124,7 +93,7 @@ impl<'db> Mro<'db> {
|
||||
// ```
|
||||
[] => {
|
||||
// e.g. `class Foo[T]: ...` implicitly has `Generic` inserted into its bases
|
||||
if class.has_pep_695_type_params(db) {
|
||||
if class.is_generic() {
|
||||
Ok(Self::from([
|
||||
ClassBase::Class(class),
|
||||
ClassBase::Generic,
|
||||
@@ -141,14 +110,13 @@ impl<'db> Mro<'db> {
|
||||
// but it's a common case (i.e., worth optimizing for),
|
||||
// and the `c3_merge` function requires lots of allocations.
|
||||
[single_base]
|
||||
if !class.has_pep_695_type_params(db)
|
||||
&& !matches!(
|
||||
single_base,
|
||||
Type::GenericAlias(_)
|
||||
| Type::KnownInstance(
|
||||
KnownInstanceType::Generic(_) | KnownInstanceType::Protocol(_)
|
||||
)
|
||||
) =>
|
||||
if !matches!(
|
||||
single_base,
|
||||
Type::GenericAlias(_)
|
||||
| Type::KnownInstance(
|
||||
KnownInstanceType::Generic(_) | KnownInstanceType::Protocol(_)
|
||||
)
|
||||
) =>
|
||||
{
|
||||
ClassBase::try_from_type(db, *single_base).map_or_else(
|
||||
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|
||||
@@ -169,21 +137,31 @@ impl<'db> Mro<'db> {
|
||||
// We'll fallback to a full implementation of the C3-merge algorithm to determine
|
||||
// what MRO Python will give this class at runtime
|
||||
// (if an MRO is indeed resolvable at all!)
|
||||
_ => {
|
||||
original_bases => {
|
||||
let mut resolved_bases = vec![];
|
||||
let mut invalid_bases = vec![];
|
||||
|
||||
for (i, base) in original_bases.iter().enumerate() {
|
||||
// Note that we emit a diagnostic for inheriting from bare (unsubscripted) `Generic` elsewhere
|
||||
// This emulates the behavior of `typing._GenericAlias.__mro_entries__` at
|
||||
// <https://github.com/python/cpython/blob/ad42dc1909bdf8ec775b63fb22ed48ff42797a17/Lib/typing.py#L1487-L1500>.
|
||||
//
|
||||
// Note that emit a diagnostic for inheriting from bare (unsubscripted) `Generic` elsewhere
|
||||
// (see `infer::TypeInferenceBuilder::check_class_definitions`),
|
||||
// which is why we only care about `KnownInstanceType::Generic(Some(_))`,
|
||||
// not `KnownInstanceType::Generic(None)`.
|
||||
if let Type::KnownInstance(KnownInstanceType::Generic(Some(_))) = base {
|
||||
maybe_add_generic(
|
||||
&mut resolved_bases,
|
||||
original_bases,
|
||||
&original_bases[i + 1..],
|
||||
);
|
||||
if original_bases
|
||||
.contains(&Type::KnownInstance(KnownInstanceType::Protocol(None)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if original_bases[i + 1..]
|
||||
.iter()
|
||||
.any(|b| b.is_generic_alias() && b != base)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
resolved_bases.push(ClassBase::Generic);
|
||||
} else {
|
||||
match ClassBase::try_from_type(db, *base) {
|
||||
Some(valid_base) => resolved_bases.push(valid_base),
|
||||
@@ -196,12 +174,6 @@ impl<'db> Mro<'db> {
|
||||
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
|
||||
}
|
||||
|
||||
// `Generic` is implicitly added to the bases list of a class that has PEP-695 type parameters
|
||||
// (documented at https://docs.python.org/3/reference/compound_stmts.html#generic-classes)
|
||||
if class.has_pep_695_type_params(db) {
|
||||
maybe_add_generic(&mut resolved_bases, original_bases, &[]);
|
||||
}
|
||||
|
||||
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
|
||||
for base in &resolved_bases {
|
||||
if base.has_cyclic_mro(db) {
|
||||
@@ -220,18 +192,6 @@ impl<'db> Mro<'db> {
|
||||
return Ok(mro);
|
||||
}
|
||||
|
||||
// We now know that the MRO is unresolvable through the C3-merge algorithm.
|
||||
// The rest of this function is dedicated to figuring out the best error message
|
||||
// to report to the user.
|
||||
|
||||
if class.has_pep_695_type_params(db)
|
||||
&& original_bases.iter().any(|base| {
|
||||
matches!(base, Type::KnownInstance(KnownInstanceType::Generic(_)))
|
||||
})
|
||||
{
|
||||
return Err(MroErrorKind::Pep695ClassWithGenericInheritance);
|
||||
}
|
||||
|
||||
let mut duplicate_dynamic_bases = false;
|
||||
|
||||
let duplicate_bases: Vec<DuplicateBaseError<'db>> = {
|
||||
@@ -456,9 +416,6 @@ pub(super) enum MroErrorKind<'db> {
|
||||
/// See [`DuplicateBaseError`] for more details.
|
||||
DuplicateBases(Box<[DuplicateBaseError<'db>]>),
|
||||
|
||||
/// The class uses PEP-695 parameters and also inherits from `Generic[]`.
|
||||
Pep695ClassWithGenericInheritance,
|
||||
|
||||
/// A cycle was encountered resolving the class' bases.
|
||||
InheritanceCycle,
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::cmp::Ordering;
|
||||
use crate::db::Db;
|
||||
|
||||
use super::{
|
||||
DynamicType, SuperOwnerKind, TodoType, Type, class_base::ClassBase,
|
||||
DynamicType, KnownInstanceType, SuperOwnerKind, TodoType, Type, class_base::ClassBase,
|
||||
subclass_of::SubclassOfInner,
|
||||
};
|
||||
|
||||
@@ -180,7 +180,144 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(_, Type::BoundSuper(_)) => Ordering::Greater,
|
||||
|
||||
(Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) => {
|
||||
left_instance.cmp(right_instance)
|
||||
match (left_instance, right_instance) {
|
||||
(KnownInstanceType::Tuple, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Tuple) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::AlwaysFalsy, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::AlwaysFalsy) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::AlwaysTruthy, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::AlwaysTruthy) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Annotated, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Annotated) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Callable, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Callable) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ChainMap, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ChainMap) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ClassVar, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ClassVar) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Concatenate, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Concatenate) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Counter, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Counter) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::DefaultDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::DefaultDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Deque, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Deque) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Dict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Dict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Final, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Final) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::FrozenSet, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::FrozenSet) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeGuard, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeGuard) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypedDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypedDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::List, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::List) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Literal, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Literal) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::LiteralString, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::LiteralString) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Optional, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Optional) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::OrderedDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::OrderedDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Generic(left), KnownInstanceType::Generic(right)) => {
|
||||
left.cmp(right)
|
||||
}
|
||||
(KnownInstanceType::Generic(_), _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Generic(_)) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Protocol(left), KnownInstanceType::Protocol(right)) => {
|
||||
left.cmp(right)
|
||||
}
|
||||
(KnownInstanceType::Protocol(_), _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Protocol(_)) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::NoReturn, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::NoReturn) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Never, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Never) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Set, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Set) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Type, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Type) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeAlias, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeAlias) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Unknown, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Unknown) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Not, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Not) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Intersection, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Intersection) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeOf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeOf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::CallableTypeOf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::CallableTypeOf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Unpack, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Unpack) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypingSelf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypingSelf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Required, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Required) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::NotRequired, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::NotRequired) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeIs, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeIs) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ReadOnly, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ReadOnly) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Union, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Union) => Ordering::Greater,
|
||||
|
||||
(
|
||||
KnownInstanceType::TypeAliasType(left),
|
||||
KnownInstanceType::TypeAliasType(right),
|
||||
) => left.cmp(right),
|
||||
(KnownInstanceType::TypeAliasType(_), _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeAliasType(_)) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeVar(left), KnownInstanceType::TypeVar(right)) => {
|
||||
left.cmp(right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Type::KnownInstance(_), _) => Ordering::Less,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
use crate::{Db, Program, PythonVersionWithSource};
|
||||
use ruff_db::{
|
||||
diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic},
|
||||
files::system_path_to_file,
|
||||
};
|
||||
|
||||
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
|
||||
///
|
||||
/// ty can infer the Python version from various sources, such as command-line arguments,
|
||||
/// configuration files, or defaults.
|
||||
pub fn add_inferred_python_version_hint_to_diagnostic(
|
||||
db: &dyn Db,
|
||||
diagnostic: &mut Diagnostic,
|
||||
action: &str,
|
||||
) {
|
||||
let program = Program::get(db);
|
||||
let PythonVersionWithSource { version, source } = program.python_version_with_source(db);
|
||||
|
||||
match source {
|
||||
crate::PythonVersionSource::Cli => {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when {action} because it was specified on the command line",
|
||||
));
|
||||
}
|
||||
crate::PythonVersionSource::File(path, range) => {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &**path) {
|
||||
let mut sub_diagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format_args!("Python {version} was assumed when {action}"),
|
||||
);
|
||||
sub_diagnostic.annotate(
|
||||
Annotation::primary(Span::from(file).with_optional_range(*range)).message(
|
||||
format_args!("Python {version} assumed due to this configuration setting"),
|
||||
),
|
||||
);
|
||||
diagnostic.sub(sub_diagnostic);
|
||||
} else {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when {action} because of your configuration file(s)",
|
||||
));
|
||||
}
|
||||
}
|
||||
crate::PythonVersionSource::Default => {
|
||||
diagnostic.info(format_args!(
|
||||
"Python {version} was assumed when {action} \
|
||||
because it is the newest Python version supported by ty, \
|
||||
and neither a command-line argument nor a configuration setting was provided",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub(crate) mod diagnostics;
|
||||
pub(crate) mod subscript;
|
||||
|
||||
@@ -199,6 +199,7 @@ impl NotebookDocument {
|
||||
}
|
||||
|
||||
/// Get the URI for a cell by its index within the cell array.
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn cell_uri_by_index(&self, index: CellId) -> Option<&lsp_types::Url> {
|
||||
self.cells.get(index).map(|cell| &cell.url)
|
||||
}
|
||||
@@ -211,7 +212,7 @@ impl NotebookDocument {
|
||||
}
|
||||
|
||||
/// Returns a list of cell URIs in the order they appear in the array.
|
||||
pub(crate) fn cell_urls(&self) -> impl Iterator<Item = &lsp_types::Url> {
|
||||
pub(crate) fn urls(&self) -> impl Iterator<Item = &lsp_types::Url> {
|
||||
self.cells.iter().map(|cell| &cell.url)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,23 @@
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::PublishDiagnostics;
|
||||
use lsp_types::{
|
||||
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
||||
NumberOrString, PublishDiagnosticsParams, Range, Url,
|
||||
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, NumberOrString,
|
||||
PublishDiagnosticsParams, Range, Url,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ty_project::{Db, ProjectDatabase};
|
||||
|
||||
use crate::DocumentSnapshot;
|
||||
use crate::PositionEncoding;
|
||||
use crate::document::{FileRangeExt, ToRangeExt};
|
||||
use crate::server::Result;
|
||||
use crate::server::client::Notifier;
|
||||
use crate::system::url_to_any_system_path;
|
||||
use crate::{DocumentSnapshot, PositionEncoding, Session};
|
||||
|
||||
use super::LSPResult;
|
||||
|
||||
/// Represents the diagnostics for a text document or a notebook document.
|
||||
pub(super) enum Diagnostics {
|
||||
TextDocument(Vec<Diagnostic>),
|
||||
|
||||
/// A map of cell URLs to the diagnostics for that cell.
|
||||
NotebookDocument(FxHashMap<Url, Vec<Diagnostic>>),
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
/// Returns the diagnostics for a text document.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the diagnostics are for a notebook document.
|
||||
pub(super) fn expect_text_document(self) -> Vec<Diagnostic> {
|
||||
match self {
|
||||
Diagnostics::TextDocument(diagnostics) => diagnostics,
|
||||
Diagnostics::NotebookDocument(_) => {
|
||||
panic!("Expected a text document diagnostics, but got notebook diagnostics")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the diagnostics for the document at `uri`.
|
||||
///
|
||||
/// This is done by notifying the client with an empty list of diagnostics for the document.
|
||||
pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> {
|
||||
notifier
|
||||
.notify::<PublishDiagnostics>(PublishDiagnosticsParams {
|
||||
@@ -57,106 +29,25 @@ pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publishes the diagnostics for the given document snapshot using the [publish diagnostics
|
||||
/// notification].
|
||||
///
|
||||
/// This function is a no-op if the client supports pull diagnostics.
|
||||
///
|
||||
/// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics
|
||||
pub(super) fn publish_diagnostics(session: &Session, url: Url, notifier: &Notifier) -> Result<()> {
|
||||
if session.client_capabilities().pull_diagnostics {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Ok(path) = url_to_any_system_path(&url) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let snapshot = session
|
||||
.take_snapshot(url.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}"))
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
let db = session.project_db_or_default(&path);
|
||||
|
||||
let Some(diagnostics) = compute_diagnostics(db, &snapshot) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Sends a notification to the client with the diagnostics for the document.
|
||||
let publish_diagnostics_notification = |uri: Url, diagnostics: Vec<Diagnostic>| {
|
||||
notifier
|
||||
.notify::<PublishDiagnostics>(PublishDiagnosticsParams {
|
||||
uri,
|
||||
diagnostics,
|
||||
version: Some(snapshot.query().version()),
|
||||
})
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)
|
||||
};
|
||||
|
||||
match diagnostics {
|
||||
Diagnostics::TextDocument(diagnostics) => {
|
||||
publish_diagnostics_notification(url, diagnostics)?;
|
||||
}
|
||||
Diagnostics::NotebookDocument(cell_diagnostics) => {
|
||||
for (cell_url, diagnostics) in cell_diagnostics {
|
||||
publish_diagnostics_notification(cell_url, diagnostics)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn compute_diagnostics(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: &DocumentSnapshot,
|
||||
) -> Option<Diagnostics> {
|
||||
) -> Vec<Diagnostic> {
|
||||
let Some(file) = snapshot.file(db) else {
|
||||
tracing::info!(
|
||||
"No file found for snapshot for `{}`",
|
||||
snapshot.query().file_url()
|
||||
);
|
||||
return None;
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let diagnostics = db.check_file(file);
|
||||
|
||||
if let Some(notebook) = snapshot.query().as_notebook() {
|
||||
let mut cell_diagnostics: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
|
||||
|
||||
// Populates all relevant URLs with an empty diagnostic list. This ensures that documents
|
||||
// without diagnostics still get updated.
|
||||
for cell_url in notebook.cell_urls() {
|
||||
cell_diagnostics.entry(cell_url.clone()).or_default();
|
||||
}
|
||||
|
||||
for (cell_index, diagnostic) in diagnostics.iter().map(|diagnostic| {
|
||||
(
|
||||
// TODO: Use the cell index instead using `SourceKind`
|
||||
usize::default(),
|
||||
to_lsp_diagnostic(db, diagnostic, snapshot.encoding()),
|
||||
)
|
||||
}) {
|
||||
let Some(cell_uri) = notebook.cell_uri_by_index(cell_index) else {
|
||||
tracing::warn!("Unable to find notebook cell at index {cell_index}");
|
||||
continue;
|
||||
};
|
||||
cell_diagnostics
|
||||
.entry(cell_uri.clone())
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
|
||||
Some(Diagnostics::NotebookDocument(cell_diagnostics))
|
||||
} else {
|
||||
Some(Diagnostics::TextDocument(
|
||||
diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| to_lsp_diagnostic(db, diagnostic, snapshot.encoding()))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
diagnostics
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|message| to_lsp_diagnostic(db, message, snapshot.encoding()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP
|
||||
@@ -200,8 +91,9 @@ fn to_lsp_diagnostic(
|
||||
.id()
|
||||
.is_lint()
|
||||
.then(|| {
|
||||
Some(CodeDescription {
|
||||
href: Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id())).ok()?,
|
||||
Some(lsp_types::CodeDescription {
|
||||
href: lsp_types::Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id()))
|
||||
.ok()?,
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::DidChangeTextDocumentParams;
|
||||
use lsp_types::notification::DidChangeTextDocument;
|
||||
use lsp_types::{DidChangeTextDocumentParams, VersionedTextDocumentIdentifier};
|
||||
|
||||
use ty_project::watch::ChangeEvent;
|
||||
|
||||
use crate::server::Result;
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::api::diagnostics::publish_diagnostics;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::session::Session;
|
||||
@@ -21,23 +20,18 @@ impl NotificationHandler for DidChangeTextDocumentHandler {
|
||||
impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||
fn run(
|
||||
session: &mut Session,
|
||||
notifier: Notifier,
|
||||
_notifier: Notifier,
|
||||
_requester: &mut Requester,
|
||||
params: DidChangeTextDocumentParams,
|
||||
) -> Result<()> {
|
||||
let DidChangeTextDocumentParams {
|
||||
text_document: VersionedTextDocumentIdentifier { uri, version },
|
||||
content_changes,
|
||||
} = params;
|
||||
|
||||
let Ok(path) = url_to_any_system_path(&uri) else {
|
||||
let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let key = session.key_from_url(uri.clone());
|
||||
let key = session.key_from_url(params.text_document.uri);
|
||||
|
||||
session
|
||||
.update_text_document(&key, content_changes, version)
|
||||
.update_text_document(&key, params.content_changes, params.text_document.version)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
match path {
|
||||
@@ -54,6 +48,8 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
publish_diagnostics(session, uri, ¬ifier)
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::server::Result;
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::api::diagnostics::publish_diagnostics;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::schedule::Task;
|
||||
@@ -21,7 +20,7 @@ impl NotificationHandler for DidChangeWatchedFiles {
|
||||
impl SyncNotificationHandler for DidChangeWatchedFiles {
|
||||
fn run(
|
||||
session: &mut Session,
|
||||
notifier: Notifier,
|
||||
_notifier: Notifier,
|
||||
requester: &mut Requester,
|
||||
params: types::DidChangeWatchedFilesParams,
|
||||
) -> Result<()> {
|
||||
@@ -86,35 +85,19 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut project_changed = false;
|
||||
|
||||
for (root, changes) in events_by_db {
|
||||
tracing::debug!("Applying changes to `{root}`");
|
||||
|
||||
// SAFETY: Only paths that are part of the workspace are registered for file watching.
|
||||
// So, virtual paths and paths that are outside of a workspace does not trigger this
|
||||
// notification.
|
||||
let db = session.project_db_for_path_mut(&*root).unwrap();
|
||||
|
||||
let result = db.apply_changes(changes, None);
|
||||
|
||||
project_changed |= result.project_changed();
|
||||
db.apply_changes(changes, None);
|
||||
}
|
||||
|
||||
let client_capabilities = session.client_capabilities();
|
||||
|
||||
if project_changed {
|
||||
if client_capabilities.diagnostics_refresh {
|
||||
requester
|
||||
.request::<types::request::WorkspaceDiagnosticRefresh>((), |()| Task::nothing())
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
} else {
|
||||
for url in session.text_document_urls() {
|
||||
publish_diagnostics(session, url.clone(), ¬ifier)?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics)
|
||||
if client_capabilities.diagnostics_refresh {
|
||||
requester
|
||||
.request::<types::request::WorkspaceDiagnosticRefresh>((), |()| Task::nothing())
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
}
|
||||
|
||||
if client_capabilities.inlay_refresh {
|
||||
|
||||
@@ -6,7 +6,6 @@ use ty_project::watch::ChangeEvent;
|
||||
|
||||
use crate::TextDocument;
|
||||
use crate::server::Result;
|
||||
use crate::server::api::diagnostics::publish_diagnostics;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::session::Session;
|
||||
@@ -21,7 +20,7 @@ impl NotificationHandler for DidOpenTextDocumentHandler {
|
||||
impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
||||
fn run(
|
||||
session: &mut Session,
|
||||
notifier: Notifier,
|
||||
_notifier: Notifier,
|
||||
_requester: &mut Requester,
|
||||
DidOpenTextDocumentParams {
|
||||
text_document:
|
||||
@@ -38,22 +37,24 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
||||
};
|
||||
|
||||
let document = TextDocument::new(text, version).with_language_id(&language_id);
|
||||
session.open_text_document(uri.clone(), document);
|
||||
session.open_text_document(uri, document);
|
||||
|
||||
match &path {
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.project_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path.clone())], None);
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
db.files().virtual_file(db, virtual_path);
|
||||
db.files().virtual_file(db, &virtual_path);
|
||||
}
|
||||
}
|
||||
|
||||
publish_diagnostics(session, uri, ¬ifier)
|
||||
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use lsp_types::{
|
||||
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport,
|
||||
};
|
||||
|
||||
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
|
||||
use crate::server::api::diagnostics::compute_diagnostics;
|
||||
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
|
||||
use crate::server::{Result, client::Notifier};
|
||||
use crate::session::DocumentSnapshot;
|
||||
@@ -29,15 +29,14 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
_notifier: Notifier,
|
||||
_params: DocumentDiagnosticParams,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
let diagnostics = compute_diagnostics(db, &snapshot);
|
||||
|
||||
Ok(DocumentDiagnosticReportResult::Report(
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
// SAFETY: Pull diagnostic requests are only called for text documents, not for
|
||||
// notebook documents.
|
||||
items: compute_diagnostics(db, &snapshot)
|
||||
.map_or_else(Vec::new, Diagnostics::expect_text_document),
|
||||
items: diagnostics,
|
||||
},
|
||||
}),
|
||||
))
|
||||
|
||||
@@ -46,7 +46,6 @@ pub struct Session {
|
||||
|
||||
/// The global position encoding, negotiated during LSP initialization.
|
||||
position_encoding: PositionEncoding,
|
||||
|
||||
/// Tracks what LSP features the client supports and doesn't support.
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
}
|
||||
@@ -91,14 +90,6 @@ impl Session {
|
||||
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
|
||||
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437
|
||||
|
||||
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path,
|
||||
/// or the default project if no project is found for the path.
|
||||
pub(crate) fn project_db_or_default(&self, path: &AnySystemPath) -> &ProjectDatabase {
|
||||
path.as_system()
|
||||
.and_then(|path| self.project_db_for_path(path.as_std_path()))
|
||||
.unwrap_or_else(|| self.default_project_db())
|
||||
}
|
||||
|
||||
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if
|
||||
/// any.
|
||||
pub(crate) fn project_db_for_path(&self, path: impl AsRef<Path>) -> Option<&ProjectDatabase> {
|
||||
@@ -150,11 +141,6 @@ impl Session {
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterates over the LSP URLs for all open text documents. These URLs are valid file paths.
|
||||
pub(super) fn text_document_urls(&self) -> impl Iterator<Item = &Url> + '_ {
|
||||
self.index().text_document_urls()
|
||||
}
|
||||
|
||||
/// Registers a notebook document at the provided `url`.
|
||||
/// If a document is already open here, it will be overwritten.
|
||||
pub fn open_notebook_document(&mut self, url: Url, document: NotebookDocument) {
|
||||
|
||||
@@ -8,12 +8,7 @@ pub(crate) struct ResolvedClientCapabilities {
|
||||
pub(crate) document_changes: bool,
|
||||
pub(crate) diagnostics_refresh: bool,
|
||||
pub(crate) inlay_refresh: bool,
|
||||
|
||||
/// Whether [pull diagnostics] is supported.
|
||||
///
|
||||
/// [pull diagnostics]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics
|
||||
pub(crate) pull_diagnostics: bool,
|
||||
|
||||
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
|
||||
pub(crate) type_definition_link_support: bool,
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ impl Index {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
pub(super) fn text_document_urls(&self) -> impl Iterator<Item = &Url> + '_ {
|
||||
self.documents
|
||||
.iter()
|
||||
@@ -134,7 +135,7 @@ impl Index {
|
||||
}
|
||||
|
||||
pub(super) fn open_notebook_document(&mut self, notebook_url: Url, document: NotebookDocument) {
|
||||
for cell_url in document.cell_urls() {
|
||||
for cell_url in document.urls() {
|
||||
self.notebook_cells
|
||||
.insert(cell_url.clone(), notebook_url.clone());
|
||||
}
|
||||
|
||||
@@ -47,21 +47,12 @@ pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> {
|
||||
}
|
||||
|
||||
/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AnySystemPath {
|
||||
System(SystemPathBuf),
|
||||
SystemVirtual(SystemVirtualPathBuf),
|
||||
}
|
||||
|
||||
impl AnySystemPath {
|
||||
pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> {
|
||||
match self {
|
||||
AnySystemPath::System(system_path_buf) => Some(system_path_buf),
|
||||
AnySystemPath::SystemVirtual(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LSPSystem {
|
||||
/// A read-only copy of the index where the server stores all the open documents and settings.
|
||||
|
||||
2
ruff.schema.json
generated
2
ruff.schema.json
generated
@@ -4299,8 +4299,6 @@
|
||||
"UP046",
|
||||
"UP047",
|
||||
"UP049",
|
||||
"UP05",
|
||||
"UP050",
|
||||
"W",
|
||||
"W1",
|
||||
"W19",
|
||||
|
||||
14
ty.schema.json
generated
14
ty.schema.json
generated
@@ -14,6 +14,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"respect-ignore-files": {
|
||||
"description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"description": "Configures the enabled rules and their severity.\n\nSee [the rules documentation](https://ty.dev/rules) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule. * `warn`: Enable the rule and create a warning diagnostic. * `error`: Enable the rule and create an error diagnostic. ty will exit with a non-zero code if any error diagnostics are emitted.",
|
||||
"anyOf": [
|
||||
@@ -851,13 +858,6 @@
|
||||
"SrcOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"respect-ignore-files": {
|
||||
"description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"root": {
|
||||
"description": "The root of the project, used for finding first-party modules.\n\nIf left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.",
|
||||
"type": [
|
||||
|
||||
Reference in New Issue
Block a user