Compare commits
16 Commits
0.14.10
...
zb/debug-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b4fab764 | ||
|
|
abf17c6ef4 | ||
|
|
b9f65213d0 | ||
|
|
ff2553665c | ||
|
|
8ebbe6b0f6 | ||
|
|
6bc88c90b2 | ||
|
|
3c694c7d86 | ||
|
|
1603948aae | ||
|
|
cb6ba23b0a | ||
|
|
a918833d19 | ||
|
|
bcf9295973 | ||
|
|
5a2d3cda3d | ||
|
|
fa57253980 | ||
|
|
b7fbd986bc | ||
|
|
3d334a313e | ||
|
|
2e44a861cb |
1
.github/mypy-primer-ty.toml
vendored
1
.github/mypy-primer-ty.toml
vendored
@@ -4,5 +4,6 @@
|
||||
# Enable off-by-default rules.
|
||||
[rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
possibly-missing-import = "warn"
|
||||
unused-ignore-comment = "warn"
|
||||
division-by-zero = "warn"
|
||||
|
||||
@@ -86,3 +86,26 @@ def f():
|
||||
# Multiple codes but none are used
|
||||
# ruff: disable[E741, F401, F841]
|
||||
print("hello")
|
||||
|
||||
|
||||
def f():
|
||||
# Unknown rule codes
|
||||
# ruff: disable[YF829]
|
||||
# ruff: disable[F841, RQW320]
|
||||
value = 0
|
||||
# ruff: enable[F841, RQW320]
|
||||
# ruff: enable[YF829]
|
||||
|
||||
|
||||
def f():
|
||||
# External rule codes should be ignored
|
||||
# ruff: disable[TK421]
|
||||
print("hello")
|
||||
# ruff: enable[TK421]
|
||||
|
||||
|
||||
def f():
|
||||
# Empty or missing rule codes
|
||||
# ruff: disable
|
||||
# ruff: disable[]
|
||||
print("hello")
|
||||
|
||||
@@ -1064,6 +1064,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
|
||||
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
|
||||
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,
|
||||
(Ruff, "103") => rules::ruff::rules::InvalidSuppressionComment,
|
||||
(Ruff, "104") => rules::ruff::rules::UnmatchedSuppressionComment,
|
||||
|
||||
(Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml,
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
|
||||
@@ -313,12 +313,20 @@ mod tests {
|
||||
Rule::UnusedVariable,
|
||||
Rule::AmbiguousVariableName,
|
||||
Rule::UnusedNOQA,
|
||||
]),
|
||||
Rule::InvalidRuleCode,
|
||||
Rule::InvalidSuppressionComment,
|
||||
Rule::UnmatchedSuppressionComment,
|
||||
])
|
||||
.with_external_rules(&["TK421"]),
|
||||
&settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedVariable,
|
||||
Rule::AmbiguousVariableName,
|
||||
Rule::UnusedNOQA,
|
||||
Rule::InvalidRuleCode,
|
||||
Rule::InvalidSuppressionComment,
|
||||
Rule::UnmatchedSuppressionComment,
|
||||
])
|
||||
.with_external_rules(&["TK421"])
|
||||
.with_preview_mode(),
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -9,6 +9,21 @@ use crate::registry::Rule;
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum InvalidRuleCodeKind {
|
||||
Noqa,
|
||||
Suppression,
|
||||
}
|
||||
|
||||
impl InvalidRuleCodeKind {
|
||||
fn as_str(&self) -> &str {
|
||||
match self {
|
||||
InvalidRuleCodeKind::Noqa => "`# noqa`",
|
||||
InvalidRuleCodeKind::Suppression => "suppression",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `noqa` codes that are invalid.
|
||||
///
|
||||
@@ -36,12 +51,17 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
#[violation_metadata(preview_since = "0.11.4")]
|
||||
pub(crate) struct InvalidRuleCode {
|
||||
pub(crate) rule_code: String,
|
||||
pub(crate) kind: InvalidRuleCodeKind,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for InvalidRuleCode {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Invalid rule code in `# noqa`: {}", self.rule_code)
|
||||
format!(
|
||||
"Invalid rule code in {}: {}",
|
||||
self.kind.as_str(),
|
||||
self.rule_code
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
@@ -61,7 +81,9 @@ pub(crate) fn invalid_noqa_code(
|
||||
continue;
|
||||
};
|
||||
|
||||
let all_valid = directive.iter().all(|code| code_is_valid(code, external));
|
||||
let all_valid = directive
|
||||
.iter()
|
||||
.all(|code| code_is_valid(code.as_str(), external));
|
||||
|
||||
if all_valid {
|
||||
continue;
|
||||
@@ -69,7 +91,7 @@ pub(crate) fn invalid_noqa_code(
|
||||
|
||||
let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive
|
||||
.iter()
|
||||
.partition(|&code| code_is_valid(code, external));
|
||||
.partition(|&code| code_is_valid(code.as_str(), external));
|
||||
|
||||
if valid_codes.is_empty() {
|
||||
all_codes_invalid_diagnostic(directive, invalid_codes, context);
|
||||
@@ -81,10 +103,9 @@ pub(crate) fn invalid_noqa_code(
|
||||
}
|
||||
}
|
||||
|
||||
fn code_is_valid(code: &Code, external: &[String]) -> bool {
|
||||
let code_str = code.as_str();
|
||||
Rule::from_code(get_redirect_target(code_str).unwrap_or(code_str)).is_ok()
|
||||
|| external.iter().any(|ext| code_str.starts_with(ext))
|
||||
pub(crate) fn code_is_valid(code: &str, external: &[String]) -> bool {
|
||||
Rule::from_code(get_redirect_target(code).unwrap_or(code)).is_ok()
|
||||
|| external.iter().any(|ext| code.starts_with(ext))
|
||||
}
|
||||
|
||||
fn all_codes_invalid_diagnostic(
|
||||
@@ -100,6 +121,7 @@ fn all_codes_invalid_diagnostic(
|
||||
.map(Code::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
kind: InvalidRuleCodeKind::Noqa,
|
||||
},
|
||||
directive.range(),
|
||||
)
|
||||
@@ -116,6 +138,7 @@ fn some_codes_are_invalid_diagnostic(
|
||||
.report_diagnostic(
|
||||
InvalidRuleCode {
|
||||
rule_code: invalid_code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Noqa,
|
||||
},
|
||||
invalid_code.range(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::AlwaysFixableViolation;
|
||||
use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for invalid suppression comments
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Invalid suppression comments are ignored by Ruff, and should either
|
||||
/// be fixed or removed to avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ruff: disable # missing codes
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # ruff: disable[E501]
|
||||
/// ```
|
||||
///
|
||||
/// Or delete the invalid suppression comment.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.11")]
|
||||
pub(crate) struct InvalidSuppressionComment {
|
||||
pub(crate) kind: InvalidSuppressionCommentKind,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for InvalidSuppressionComment {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let msg = match self.kind {
|
||||
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Indentation) => {
|
||||
"unexpected indentation".to_string()
|
||||
}
|
||||
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Trailing) => {
|
||||
"trailing comments are not supported".to_string()
|
||||
}
|
||||
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Unmatched) => {
|
||||
"no matching 'disable' comment".to_string()
|
||||
}
|
||||
InvalidSuppressionCommentKind::Error(error) => format!("{error}"),
|
||||
};
|
||||
format!("Invalid suppression comment: {msg}")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove suppression comment".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum InvalidSuppressionCommentKind {
|
||||
Invalid(InvalidSuppressionKind),
|
||||
Error(ParseErrorKind),
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub(crate) use invalid_formatter_suppression_comment::*;
|
||||
pub(crate) use invalid_index_type::*;
|
||||
pub(crate) use invalid_pyproject_toml::*;
|
||||
pub(crate) use invalid_rule_code::*;
|
||||
pub(crate) use invalid_suppression_comment::*;
|
||||
pub(crate) use legacy_form_pytest_raises::*;
|
||||
pub(crate) use logging_eager_conversion::*;
|
||||
pub(crate) use map_int_version_parsing::*;
|
||||
@@ -46,6 +47,7 @@ pub(crate) use starmap_zip::*;
|
||||
pub(crate) use static_key_dict_comprehension::*;
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
pub(crate) use test_rules::*;
|
||||
pub(crate) use unmatched_suppression_comment::*;
|
||||
pub(crate) use unnecessary_cast_to_int::*;
|
||||
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
||||
pub(crate) use unnecessary_key_check::*;
|
||||
@@ -87,6 +89,7 @@ mod invalid_formatter_suppression_comment;
|
||||
mod invalid_index_type;
|
||||
mod invalid_pyproject_toml;
|
||||
mod invalid_rule_code;
|
||||
mod invalid_suppression_comment;
|
||||
mod legacy_form_pytest_raises;
|
||||
mod logging_eager_conversion;
|
||||
mod map_int_version_parsing;
|
||||
@@ -113,6 +116,7 @@ mod static_key_dict_comprehension;
|
||||
mod suppression_comment_visitor;
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
pub(crate) mod test_rules;
|
||||
mod unmatched_suppression_comment;
|
||||
mod unnecessary_cast_to_int;
|
||||
mod unnecessary_iterable_allocation_for_first_element;
|
||||
mod unnecessary_key_check;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::Violation;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unmatched range suppression comments
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Unmatched range suppression comments can inadvertently suppress violations
|
||||
/// over larger sections of code than intended, particularly at module scope.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// # ruff: disable[E501] # unmatched
|
||||
/// REALLY_LONG_VALUES = [...]
|
||||
///
|
||||
/// print(REALLY_LONG_VALUES)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def foo():
|
||||
/// # ruff: disable[E501]
|
||||
/// REALLY_LONG_VALUES = [...]
|
||||
/// # ruff: enable[E501]
|
||||
///
|
||||
/// print(REALLY_LONG_VALUES)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.11")]
|
||||
pub(crate) struct UnmatchedSuppressionComment;
|
||||
|
||||
impl Violation for UnmatchedSuppressionComment {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Suppression comment without matching `#ruff:enable` comment".to_string()
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 14
|
||||
Added: 11
|
||||
Removed: 15
|
||||
Added: 23
|
||||
|
||||
--- Removed ---
|
||||
E741 Ambiguous variable name: `I`
|
||||
@@ -238,8 +238,60 @@ help: Remove assignment to unused variable `I`
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
|
||||
F841 [*] Local variable `value` is assigned to but never used
|
||||
--> suppressions.py:95:5
|
||||
|
|
||||
93 | # ruff: disable[YF829]
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
| ^^^^^
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
|
|
||||
help: Remove assignment to unused variable `value`
|
||||
92 | # Unknown rule codes
|
||||
93 | # ruff: disable[YF829]
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
- value = 0
|
||||
95 + pass
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
98 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
|
||||
|
||||
--- Added ---
|
||||
RUF104 Suppression comment without matching `#ruff:enable` comment
|
||||
--> suppressions.py:11:5
|
||||
|
|
||||
9 | # These should both be ignored by the implicit range suppression.
|
||||
10 | # Should also generate an "unmatched suppression" warning.
|
||||
11 | # ruff:disable[E741,F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 | I = 1
|
||||
|
|
||||
|
||||
|
||||
RUF103 [*] Invalid suppression comment: no matching 'disable' comment
|
||||
--> suppressions.py:19:5
|
||||
|
|
||||
17 | # should be generated.
|
||||
18 | I = 1
|
||||
19 | # ruff: enable[E741, F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Remove suppression comment
|
||||
16 | # Neither warning is ignored, and an "unmatched suppression"
|
||||
17 | # should be generated.
|
||||
18 | I = 1
|
||||
- # ruff: enable[E741, F841]
|
||||
19 |
|
||||
20 |
|
||||
21 | def f():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (non-enabled: `E501`)
|
||||
--> suppressions.py:46:5
|
||||
|
|
||||
@@ -298,6 +350,17 @@ help: Remove unused `noqa` directive
|
||||
58 |
|
||||
|
||||
|
||||
RUF104 Suppression comment without matching `#ruff:enable` comment
|
||||
--> suppressions.py:61:5
|
||||
|
|
||||
59 | def f():
|
||||
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
|
||||
61 | # ruff: disable[F841, F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
62 | foo = 0
|
||||
|
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (unused: `F841`)
|
||||
--> suppressions.py:61:21
|
||||
|
|
||||
@@ -318,6 +381,18 @@ help: Remove unused suppression
|
||||
64 |
|
||||
|
||||
|
||||
RUF104 Suppression comment without matching `#ruff:enable` comment
|
||||
--> suppressions.py:68:5
|
||||
|
|
||||
66 | # Overlapping range suppressions, one should be marked as used,
|
||||
67 | # and the other should trigger an unused suppression diagnostic
|
||||
68 | # ruff: disable[F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
69 | # ruff: disable[F841]
|
||||
70 | foo = 0
|
||||
|
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (unused: `F841`)
|
||||
--> suppressions.py:69:5
|
||||
|
|
||||
@@ -337,6 +412,17 @@ help: Remove unused suppression
|
||||
71 |
|
||||
|
||||
|
||||
RUF104 Suppression comment without matching `#ruff:enable` comment
|
||||
--> suppressions.py:75:5
|
||||
|
|
||||
73 | def f():
|
||||
74 | # Multiple codes but only one is used
|
||||
75 | # ruff: disable[E741, F401, F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
76 | foo = 0
|
||||
|
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (unused: `E741`)
|
||||
--> suppressions.py:75:21
|
||||
|
|
||||
@@ -377,6 +463,17 @@ help: Remove unused suppression
|
||||
78 |
|
||||
|
||||
|
||||
RUF104 Suppression comment without matching `#ruff:enable` comment
|
||||
--> suppressions.py:81:5
|
||||
|
|
||||
79 | def f():
|
||||
80 | # Multiple codes but only two are used
|
||||
81 | # ruff: disable[E741, F401, F841]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
82 | I = 0
|
||||
|
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (non-enabled: `F401`)
|
||||
--> suppressions.py:81:27
|
||||
|
|
||||
@@ -413,6 +510,8 @@ help: Remove unused suppression
|
||||
- # ruff: disable[E741, F401, F841]
|
||||
87 + # ruff: disable[F401, F841]
|
||||
88 | print("hello")
|
||||
89 |
|
||||
90 |
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (non-enabled: `F401`)
|
||||
@@ -431,6 +530,8 @@ help: Remove unused suppression
|
||||
- # ruff: disable[E741, F401, F841]
|
||||
87 + # ruff: disable[E741, F841]
|
||||
88 | print("hello")
|
||||
89 |
|
||||
90 |
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (unused: `F841`)
|
||||
@@ -449,3 +550,122 @@ help: Remove unused suppression
|
||||
- # ruff: disable[E741, F401, F841]
|
||||
87 + # ruff: disable[E741, F401]
|
||||
88 | print("hello")
|
||||
89 |
|
||||
90 |
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: YF829
|
||||
--> suppressions.py:93:21
|
||||
|
|
||||
91 | def f():
|
||||
92 | # Unknown rule codes
|
||||
93 | # ruff: disable[YF829]
|
||||
| ^^^^^
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
|
|
||||
help: Remove the rule code
|
||||
90 |
|
||||
91 | def f():
|
||||
92 | # Unknown rule codes
|
||||
- # ruff: disable[YF829]
|
||||
93 | # ruff: disable[F841, RQW320]
|
||||
94 | value = 0
|
||||
95 | # ruff: enable[F841, RQW320]
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
--> suppressions.py:94:27
|
||||
|
|
||||
92 | # Unknown rule codes
|
||||
93 | # ruff: disable[YF829]
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
| ^^^^^^
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
|
|
||||
help: Remove the rule code
|
||||
91 | def f():
|
||||
92 | # Unknown rule codes
|
||||
93 | # ruff: disable[YF829]
|
||||
- # ruff: disable[F841, RQW320]
|
||||
94 + # ruff: disable[F841]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
--> suppressions.py:96:26
|
||||
|
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
| ^^^^^^
|
||||
97 | # ruff: enable[YF829]
|
||||
|
|
||||
help: Remove the rule code
|
||||
93 | # ruff: disable[YF829]
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
- # ruff: enable[F841, RQW320]
|
||||
96 + # ruff: enable[F841]
|
||||
97 | # ruff: enable[YF829]
|
||||
98 |
|
||||
99 |
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: YF829
|
||||
--> suppressions.py:97:20
|
||||
|
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
| ^^^^^
|
||||
|
|
||||
help: Remove the rule code
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
- # ruff: enable[YF829]
|
||||
97 |
|
||||
98 |
|
||||
99 | def f():
|
||||
|
||||
|
||||
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
|
||||
--> suppressions.py:109:5
|
||||
|
|
||||
107 | def f():
|
||||
108 | # Empty or missing rule codes
|
||||
109 | # ruff: disable
|
||||
| ^^^^^^^^^^^^^^^
|
||||
110 | # ruff: disable[]
|
||||
111 | print("hello")
|
||||
|
|
||||
help: Remove suppression comment
|
||||
106 |
|
||||
107 | def f():
|
||||
108 | # Empty or missing rule codes
|
||||
- # ruff: disable
|
||||
109 | # ruff: disable[]
|
||||
110 | print("hello")
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
|
||||
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
|
||||
--> suppressions.py:110:5
|
||||
|
|
||||
108 | # Empty or missing rule codes
|
||||
109 | # ruff: disable
|
||||
110 | # ruff: disable[]
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
111 | print("hello")
|
||||
|
|
||||
help: Remove suppression comment
|
||||
107 | def f():
|
||||
108 | # Empty or missing rule codes
|
||||
109 | # ruff: disable
|
||||
- # ruff: disable[]
|
||||
110 | print("hello")
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -471,6 +471,13 @@ impl LinterSettings {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_external_rules(mut self, rules: &[&str]) -> Self {
|
||||
self.external
|
||||
.extend(rules.iter().map(std::string::ToString::to_string));
|
||||
self
|
||||
}
|
||||
|
||||
/// Resolve the [`TargetVersion`] to use for linting.
|
||||
///
|
||||
/// This method respects the per-file version overrides in
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::token::{TokenKind, Tokens};
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::cell::Cell;
|
||||
use std::{error::Error, fmt::Formatter};
|
||||
use thiserror::Error;
|
||||
@@ -17,7 +18,11 @@ use crate::checkers::ast::LintContext;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix::edits::delete_comment;
|
||||
use crate::preview::is_range_suppressions_enabled;
|
||||
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::ruff::rules::{
|
||||
InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind,
|
||||
UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, UnusedNOQAKind, code_is_valid,
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -130,7 +135,7 @@ impl Suppressions {
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.valid.is_empty()
|
||||
self.valid.is_empty() && self.invalid.is_empty() && self.errors.is_empty()
|
||||
}
|
||||
|
||||
/// Check if a diagnostic is suppressed by any known range suppressions
|
||||
@@ -150,7 +155,9 @@ impl Suppressions {
|
||||
};
|
||||
|
||||
for suppression in &self.valid {
|
||||
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
|
||||
let suppression_code =
|
||||
get_redirect_target(suppression.code.as_str()).unwrap_or(suppression.code.as_str());
|
||||
if *code == suppression_code && suppression.range.contains_range(range) {
|
||||
suppression.used.set(true);
|
||||
return true;
|
||||
}
|
||||
@@ -159,81 +166,140 @@ impl Suppressions {
|
||||
}
|
||||
|
||||
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
|
||||
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unused = self
|
||||
.valid
|
||||
.iter()
|
||||
.filter(|suppression| !suppression.used.get());
|
||||
|
||||
for suppression in unused {
|
||||
let Ok(rule) = Rule::from_code(&suppression.code) else {
|
||||
continue; // TODO: invalid code
|
||||
};
|
||||
for comment in &suppression.comments {
|
||||
let mut range = comment.range;
|
||||
let edit = if comment.codes.len() == 1 {
|
||||
delete_comment(comment.range, locator)
|
||||
} else {
|
||||
let code_index = comment
|
||||
.codes
|
||||
.iter()
|
||||
.position(|range| locator.slice(range) == suppression.code)
|
||||
.unwrap();
|
||||
range = comment.codes[code_index];
|
||||
let code_range = if code_index < (comment.codes.len() - 1) {
|
||||
TextRange::new(
|
||||
comment.codes[code_index].start(),
|
||||
comment.codes[code_index + 1].start(),
|
||||
)
|
||||
} else {
|
||||
TextRange::new(
|
||||
comment.codes[code_index - 1].end(),
|
||||
comment.codes[code_index].end(),
|
||||
)
|
||||
let mut unmatched_ranges = FxHashSet::default();
|
||||
for suppression in &self.valid {
|
||||
if !code_is_valid(&suppression.code, &context.settings().external) {
|
||||
// InvalidRuleCode
|
||||
if context.is_rule_enabled(Rule::InvalidRuleCode) {
|
||||
for comment in &suppression.comments {
|
||||
let (range, edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
comment,
|
||||
true,
|
||||
);
|
||||
context
|
||||
.report_diagnostic(
|
||||
InvalidRuleCode {
|
||||
rule_code: suppression.code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Suppression,
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
}
|
||||
} else if !suppression.used.get() {
|
||||
// UnusedNOQA
|
||||
if context.is_rule_enabled(Rule::UnusedNOQA) {
|
||||
let Ok(rule) = Rule::from_code(
|
||||
get_redirect_target(&suppression.code).unwrap_or(&suppression.code),
|
||||
) else {
|
||||
continue; // "external" lint code, don't treat it as unused
|
||||
};
|
||||
Edit::range_deletion(code_range)
|
||||
};
|
||||
for comment in &suppression.comments {
|
||||
let (range, edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
comment,
|
||||
false,
|
||||
);
|
||||
|
||||
let codes = if context.is_rule_enabled(rule) {
|
||||
UnusedCodes {
|
||||
unmatched: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
UnusedCodes {
|
||||
disabled: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let codes = if context.is_rule_enabled(rule) {
|
||||
UnusedCodes {
|
||||
unmatched: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
UnusedCodes {
|
||||
disabled: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
let mut diagnostic = context.report_diagnostic(
|
||||
UnusedNOQA {
|
||||
codes: Some(codes),
|
||||
kind: UnusedNOQAKind::Suppression,
|
||||
},
|
||||
range,
|
||||
);
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
context
|
||||
.report_diagnostic(
|
||||
UnusedNOQA {
|
||||
codes: Some(codes),
|
||||
kind: UnusedNOQAKind::Suppression,
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
}
|
||||
} else if suppression.comments.len() == 1 {
|
||||
// UnmatchedSuppressionComment
|
||||
let range = suppression.comments[0].range;
|
||||
if unmatched_ranges.insert(range) {
|
||||
context.report_diagnostic_if_enabled(UnmatchedSuppressionComment {}, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for error in self
|
||||
.errors
|
||||
.iter()
|
||||
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
|
||||
{
|
||||
let mut diagnostic = context.report_diagnostic(
|
||||
UnusedNOQA {
|
||||
codes: Some(UnusedCodes::default()),
|
||||
kind: UnusedNOQAKind::Suppression,
|
||||
},
|
||||
error.range,
|
||||
);
|
||||
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
|
||||
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
|
||||
for error in &self.errors {
|
||||
context
|
||||
.report_diagnostic(
|
||||
InvalidSuppressionComment {
|
||||
kind: InvalidSuppressionCommentKind::Error(error.kind),
|
||||
},
|
||||
error.range,
|
||||
)
|
||||
.set_fix(Fix::unsafe_edit(delete_comment(error.range, locator)));
|
||||
}
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
|
||||
for invalid in &self.invalid {
|
||||
context
|
||||
.report_diagnostic(
|
||||
InvalidSuppressionComment {
|
||||
kind: InvalidSuppressionCommentKind::Invalid(invalid.kind),
|
||||
},
|
||||
invalid.comment.range,
|
||||
)
|
||||
.set_fix(Fix::unsafe_edit(delete_comment(
|
||||
invalid.comment.range,
|
||||
locator,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_code_or_comment(
|
||||
locator: &Locator<'_>,
|
||||
suppression: &Suppression,
|
||||
comment: &SuppressionComment,
|
||||
highlight_only_code: bool,
|
||||
) -> (TextRange, Edit) {
|
||||
let mut range = comment.range;
|
||||
let edit = if comment.codes.len() == 1 {
|
||||
if highlight_only_code {
|
||||
range = comment.codes[0];
|
||||
}
|
||||
delete_comment(comment.range, locator)
|
||||
} else {
|
||||
let code_index = comment
|
||||
.codes
|
||||
.iter()
|
||||
.position(|range| locator.slice(range) == suppression.code)
|
||||
.unwrap();
|
||||
range = comment.codes[code_index];
|
||||
let code_range = if code_index < (comment.codes.len() - 1) {
|
||||
TextRange::new(
|
||||
comment.codes[code_index].start(),
|
||||
comment.codes[code_index + 1].start(),
|
||||
)
|
||||
} else {
|
||||
TextRange::new(
|
||||
comment.codes[code_index - 1].end(),
|
||||
comment.codes[code_index].end(),
|
||||
)
|
||||
};
|
||||
Edit::range_deletion(code_range)
|
||||
};
|
||||
(range, edit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +457,7 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
|
||||
enum ParseErrorKind {
|
||||
pub(crate) enum ParseErrorKind {
|
||||
#[error("not a suppression comment")]
|
||||
NotASuppression,
|
||||
|
||||
@@ -401,7 +467,7 @@ enum ParseErrorKind {
|
||||
#[error("unknown ruff directive")]
|
||||
UnknownAction,
|
||||
|
||||
#[error("missing suppression codes")]
|
||||
#[error("missing suppression codes like `[E501, ...]`")]
|
||||
MissingCodes,
|
||||
|
||||
#[error("missing closing bracket")]
|
||||
|
||||
64
crates/ty/docs/rules.md
generated
64
crates/ty/docs/rules.md
generated
@@ -2511,38 +2511,6 @@ class A:
|
||||
A()[0] # TypeError: 'A' object is not subscriptable
|
||||
```
|
||||
|
||||
## `possibly-missing-import`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1575" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for imports of symbols that may be missing.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Importing a missing module or name will raise a `ModuleNotFoundError`
|
||||
or `ImportError` at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
# module.py
|
||||
import datetime
|
||||
|
||||
if datetime.date.today().weekday() != 6:
|
||||
a = 1
|
||||
|
||||
# main.py
|
||||
from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
```
|
||||
|
||||
## `redundant-cast`
|
||||
|
||||
<small>
|
||||
@@ -2778,6 +2746,38 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
5 / 0
|
||||
```
|
||||
|
||||
## `possibly-missing-import`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1575" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for imports of symbols that may be missing.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Importing a missing module or name will raise a `ModuleNotFoundError`
|
||||
or `ImportError` at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
# module.py
|
||||
import datetime
|
||||
|
||||
if datetime.date.today().weekday() != 6:
|
||||
a = 1
|
||||
|
||||
# main.py
|
||||
from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
```
|
||||
|
||||
## `possibly-unresolved-reference`
|
||||
|
||||
<small>
|
||||
|
||||
@@ -1726,5 +1726,298 @@ reveal_type(actual_td) # revealed: ActualTypedDict
|
||||
reveal_type(actual_td["name"]) # revealed: str
|
||||
```
|
||||
|
||||
## Disjointness with other `TypedDict`s
|
||||
|
||||
Two `TypedDict` types are disjoint if it's impossible to come up with a third (fully-static)
|
||||
`TypedDict` that's assignable to both. The simplest way to establish this is if both sides have
|
||||
fields with the same name but disjoint types:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, final
|
||||
from typing_extensions import ReadOnly
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
# Two simple disjoint types, to avoid relying on `@disjoint_base` special cases for built-ins like
|
||||
# `int` and `str`.
|
||||
@final
|
||||
class Final1: ...
|
||||
|
||||
@final
|
||||
class Final2: ...
|
||||
|
||||
static_assert(is_disjoint_from(Final1, Final2))
|
||||
|
||||
class DisjointTD1(TypedDict):
|
||||
# Make this example `ReadOnly` because that actually ends up checking the field types for
|
||||
# disjointness in practice. Mutable fields are stricter. We'll get to that below.
|
||||
disjoint: ReadOnly[Final1]
|
||||
# While we're here: It doesn't matter how many other compatible fields there are. Just the one
|
||||
# incompatible field above establishes disjointness.
|
||||
common1: object
|
||||
common2: object
|
||||
|
||||
class DisjointTD2(TypedDict):
|
||||
disjoint: ReadOnly[Final2]
|
||||
common1: object
|
||||
common2: object
|
||||
|
||||
static_assert(is_disjoint_from(DisjointTD1, DisjointTD2))
|
||||
```
|
||||
|
||||
However, note that most pairs of non-final classes are *not* disjoint from each other, even if
|
||||
neither inherits from the other, because we could define a third class that multiply-inherits from
|
||||
both. `TypedDict` disjointness takes this into account. For example:
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to
|
||||
|
||||
class NonFinal1: ...
|
||||
class NonFinal2: ...
|
||||
class CommonSub(NonFinal1, NonFinal2): ...
|
||||
|
||||
static_assert(not is_disjoint_from(NonFinal1, NonFinal2))
|
||||
static_assert(not is_assignable_to(NonFinal1, NonFinal2))
|
||||
static_assert(is_assignable_to(CommonSub, NonFinal1))
|
||||
static_assert(is_assignable_to(CommonSub, NonFinal2))
|
||||
|
||||
class NonDisjointTD1(TypedDict):
|
||||
non_disjoint: ReadOnly[NonFinal1]
|
||||
# While we're here: It doesn't matter how many "extra" fields there are, or what order the
|
||||
# fields are in. Only shared field names can establish disjointness.
|
||||
extra1: int
|
||||
|
||||
class NonDisjointTD2(TypedDict):
|
||||
extra2: str
|
||||
non_disjoint: ReadOnly[NonFinal2]
|
||||
|
||||
class CommonSubTD(TypedDict):
|
||||
extra2: str
|
||||
extra1: int
|
||||
non_disjoint: ReadOnly[CommonSub]
|
||||
|
||||
# The first two TDs above are not assignable in either direction...
|
||||
static_assert(not is_assignable_to(NonDisjointTD1, NonDisjointTD2))
|
||||
static_assert(not is_assignable_to(NonDisjointTD2, NonDisjointTD1))
|
||||
# ...but they're still not disjoint...
|
||||
static_assert(not is_disjoint_from(NonDisjointTD1, NonDisjointTD2))
|
||||
# ...because the third TD above is assignable to both of them.
|
||||
static_assert(is_assignable_to(CommonSubTD, NonDisjointTD1))
|
||||
static_assert(is_assignable_to(CommonSubTD, NonDisjointTD2))
|
||||
static_assert(not is_disjoint_from(CommonSubTD, NonDisjointTD1))
|
||||
static_assert(not is_disjoint_from(CommonSubTD, NonDisjointTD2))
|
||||
```
|
||||
|
||||
We made the important fields `ReadOnly` above, because those only establish disjointness when
|
||||
they're disjoint themselves. However, the rules for mutable fields are stricter. Mutable fields in
|
||||
common need to have *compatible* types (in the fully-static case, equivalent types):
|
||||
|
||||
```py
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
class IntTD(TypedDict):
|
||||
x: int
|
||||
|
||||
class BoolTD(TypedDict):
|
||||
x: bool
|
||||
|
||||
# `bool` is assignable to `int`, but `int` is not assignable to `bool`. If `x` was `ReadOnly` (even,
|
||||
# as we'll see below, only on the `int` side), then these two TDs would not be disjoint, but in this
|
||||
# mutable case they are.
|
||||
|
||||
static_assert(is_disjoint_from(IntTD, BoolTD))
|
||||
static_assert(is_disjoint_from(BoolTD, IntTD))
|
||||
|
||||
# Gradual types: `int` is compatible with `bool | Any`, because that could materialize to
|
||||
# `bool | int`, which is just `int`. (And `int | Any` and `bool | Any` are compatible with each
|
||||
# other for the same reason.) However, `bool` is *not* compatible with `int | Any`, because there's
|
||||
# no materialization that's equivalent to `bool`.
|
||||
|
||||
class IntOrAnyTD(TypedDict):
|
||||
x: int | Any
|
||||
|
||||
class BoolOrAnyTD(TypedDict):
|
||||
x: bool | Any
|
||||
|
||||
static_assert(not is_disjoint_from(IntTD, IntOrAnyTD))
|
||||
static_assert(not is_disjoint_from(IntOrAnyTD, IntTD))
|
||||
static_assert(not is_disjoint_from(IntTD, BoolOrAnyTD))
|
||||
static_assert(not is_disjoint_from(BoolOrAnyTD, IntTD))
|
||||
|
||||
static_assert(not is_disjoint_from(IntOrAnyTD, BoolOrAnyTD))
|
||||
static_assert(not is_disjoint_from(BoolOrAnyTD, IntOrAnyTD))
|
||||
|
||||
static_assert(is_disjoint_from(BoolTD, IntOrAnyTD))
|
||||
static_assert(is_disjoint_from(IntOrAnyTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, BoolOrAnyTD))
|
||||
static_assert(not is_disjoint_from(BoolOrAnyTD, BoolTD))
|
||||
|
||||
# `Any` is compatible with everything.
|
||||
|
||||
class AnyTD(TypedDict):
|
||||
x: Any
|
||||
|
||||
static_assert(not is_disjoint_from(IntTD, AnyTD))
|
||||
static_assert(not is_disjoint_from(AnyTD, IntTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, AnyTD))
|
||||
static_assert(not is_disjoint_from(AnyTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(IntOrAnyTD, AnyTD))
|
||||
static_assert(not is_disjoint_from(AnyTD, IntOrAnyTD))
|
||||
static_assert(not is_disjoint_from(BoolOrAnyTD, AnyTD))
|
||||
static_assert(not is_disjoint_from(AnyTD, BoolOrAnyTD))
|
||||
static_assert(not is_disjoint_from(AnyTD, AnyTD))
|
||||
|
||||
# This works with generic `TypedDict`s too.
|
||||
|
||||
class TwoIntsTD(TypedDict):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
class TwoBoolsTD(TypedDict):
|
||||
x: bool
|
||||
y: bool
|
||||
|
||||
class IntBoolTD(TypedDict):
|
||||
x: int
|
||||
y: bool
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class TwoGenericTD(TypedDict, Generic[T]):
|
||||
x: T
|
||||
y: T
|
||||
|
||||
static_assert(not is_disjoint_from(TwoGenericTD[Any], TwoIntsTD))
|
||||
static_assert(not is_disjoint_from(TwoGenericTD[int], TwoIntsTD))
|
||||
static_assert(is_disjoint_from(TwoGenericTD[bool], TwoIntsTD))
|
||||
static_assert(not is_disjoint_from(TwoGenericTD[Any], TwoBoolsTD))
|
||||
static_assert(is_disjoint_from(TwoGenericTD[int], TwoBoolsTD))
|
||||
static_assert(not is_disjoint_from(TwoGenericTD[bool], TwoBoolsTD))
|
||||
# TODO: T can't be compatible with both `int` and `bool` at the same time, so these types should be
|
||||
# disjoint, regardless of the materialization of `T`.
|
||||
static_assert(not is_disjoint_from(TwoGenericTD[Any], IntBoolTD))
|
||||
```
|
||||
|
||||
If one side is mutable but the other is not, then a "third `TypedDict` that's assignable to both"
|
||||
would have to have the same type as the mutable side, so we establish disjointness if that type
|
||||
isn't assignable to the immutable side:
|
||||
|
||||
```py
|
||||
class ReadOnlyIntTD(TypedDict):
|
||||
x: ReadOnly[int]
|
||||
|
||||
class ReadOnlyBoolTD(TypedDict):
|
||||
x: ReadOnly[bool]
|
||||
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyBoolTD, ReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, ReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, BoolTD))
|
||||
static_assert(is_disjoint_from(IntTD, ReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(ReadOnlyBoolTD, IntTD))
|
||||
```
|
||||
|
||||
With mutability above we were able to make the simplifying assumption that the "third `TypedDict`
|
||||
that's assignable to both" has only mutable fields, because a mutable field is always assignable to
|
||||
its immutable counterpart. However, `Required` vs `NotRequired` are more complicated, because a a
|
||||
`Required` field is *not* necessarily assignable to its `NotRequired` counterpart. In particular, if
|
||||
a `NotRequired` field is also mutable (intuitively, if we're allowed to `del` it), then no
|
||||
`Required` field is ever assignable to it. So, if either side is `NotRequired` and mutable, and the
|
||||
other side is `Required` (regardless of mutability), then that's sufficient to establish
|
||||
disjointness:
|
||||
|
||||
```py
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
class NotRequiredIntTD(TypedDict):
|
||||
x: NotRequired[int]
|
||||
|
||||
class NotRequiredReadOnlyIntTD(TypedDict):
|
||||
x: NotRequired[ReadOnly[int]]
|
||||
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, IntTD))
|
||||
static_assert(is_disjoint_from(IntTD, NotRequiredIntTD))
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, ReadOnlyIntTD))
|
||||
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredIntTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredIntTD))
|
||||
```
|
||||
|
||||
All those rules put together give us the "full disjointness table". We've pretty well tested above
|
||||
that disjointness is symmetrical, so here we won't worry about asserting both directions for each
|
||||
check:
|
||||
|
||||
```py
|
||||
class NotRequiredBoolTD(TypedDict):
|
||||
x: NotRequired[bool]
|
||||
|
||||
class NotRequiredReadOnlyBoolTD(TypedDict):
|
||||
x: NotRequired[ReadOnly[bool]]
|
||||
|
||||
static_assert(not is_disjoint_from(IntTD, IntTD))
|
||||
static_assert(is_disjoint_from(IntTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(IntTD, ReadOnlyIntTD))
|
||||
static_assert(is_disjoint_from(IntTD, ReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(IntTD, NotRequiredIntTD))
|
||||
static_assert(is_disjoint_from(IntTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(IntTD, NotRequiredReadOnlyIntTD))
|
||||
static_assert(is_disjoint_from(IntTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredIntTD))
|
||||
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, NotRequiredReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyIntTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, BoolTD))
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, ReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredIntTD))
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyIntTD))
|
||||
static_assert(is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, ReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredReadOnlyIntTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, BoolTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, ReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(BoolTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(BoolTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyBoolTD, ReadOnlyBoolTD))
|
||||
static_assert(is_disjoint_from(ReadOnlyBoolTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(ReadOnlyBoolTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredBoolTD, NotRequiredBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredBoolTD, NotRequiredReadOnlyBoolTD))
|
||||
static_assert(not is_disjoint_from(NotRequiredReadOnlyBoolTD, NotRequiredReadOnlyBoolTD))
|
||||
```
|
||||
|
||||
## Disjointness with other types
|
||||
|
||||
```py
|
||||
from typing import TypedDict, Mapping
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class TD(TypedDict):
|
||||
x: int
|
||||
|
||||
class RegularNonTD: ...
|
||||
|
||||
static_assert(not is_disjoint_from(TD, object))
|
||||
static_assert(not is_disjoint_from(TD, Mapping[str, object]))
|
||||
static_assert(is_disjoint_from(TD, Mapping[int, object]))
|
||||
static_assert(is_disjoint_from(TD, RegularNonTD))
|
||||
|
||||
# TODO: We approximate disjointness with other types `T` by asking whether `dict[str, Any]` is
|
||||
# assignable to `T`. That covers common cases like the ones above, but does it have some false
|
||||
# negatives with `dict` types. A `TypedDict` is almost never assignable to a `dict` (or vice versa),
|
||||
# even when all of the `TypedDict`'s field types match the `dict`'s value type (and are mutable).
|
||||
# The problem is that the `TypedDict` could have been assigned to from *another* `TypedDict` with
|
||||
# additional fields, and we don't usually know anything about the types or mutability of those. On
|
||||
# the other hand, the assignment to `dict` can be allowed if the `TypedDict` has mutable
|
||||
# `extra_items` of a compatible type. See: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-with-dict
|
||||
static_assert(is_disjoint_from(TD, dict[str, int])) # error: [static-assert-error]
|
||||
static_assert(is_disjoint_from(TD, dict[str, str])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
||||
@@ -1952,8 +1952,22 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// See [`TypeRelation::Subtyping`] for more details.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.when_subtype_of(db, target, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
#[salsa::tracked(cycle_initial=is_subtype_of_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn is_subtype_of_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_ty: Type<'db>,
|
||||
target: Type<'db>,
|
||||
) -> bool {
|
||||
self_ty
|
||||
.when_subtype_of(db, target, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
}
|
||||
|
||||
if self == target {
|
||||
return true;
|
||||
}
|
||||
|
||||
is_subtype_of_impl(db, self, target)
|
||||
}
|
||||
|
||||
fn when_subtype_of(
|
||||
@@ -1988,8 +2002,22 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// See `TypeRelation::Assignability` for more details.
|
||||
pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.when_assignable_to(db, target, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
#[salsa::tracked(cycle_initial=is_assignable_to_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn is_assignable_to_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_ty: Type<'db>,
|
||||
target: Type<'db>,
|
||||
) -> bool {
|
||||
self_ty
|
||||
.when_assignable_to(db, target, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
}
|
||||
|
||||
if self == target {
|
||||
return true;
|
||||
}
|
||||
|
||||
is_assignable_to_impl(db, self, target)
|
||||
}
|
||||
|
||||
/// Return true if this type is assignable to type `target` using constraint-set assignability.
|
||||
@@ -2403,6 +2431,9 @@ impl<'db> Type<'db> {
|
||||
// `Never` is the bottom type, the empty set.
|
||||
(_, Type::Never) => ConstraintSet::from(false),
|
||||
|
||||
// Short-circuit: if both sides are the same union, they trivially satisfy the relation.
|
||||
(Type::Union(left), Type::Union(right)) if left == right => ConstraintSet::from(true),
|
||||
|
||||
(Type::Union(union), _) => union.elements(db).iter().when_all(db, |&elem_ty| {
|
||||
elem_ty.has_relation_to_impl(
|
||||
db,
|
||||
@@ -2414,16 +2445,22 @@ impl<'db> Type<'db> {
|
||||
)
|
||||
}),
|
||||
|
||||
(_, Type::Union(union)) => union.elements(db).iter().when_any(db, |&elem_ty| {
|
||||
self.has_relation_to_impl(
|
||||
db,
|
||||
elem_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
}),
|
||||
(_, Type::Union(union)) => {
|
||||
// Fast path: if self is directly a member of the union, no need to check relations
|
||||
if union.elements(db).contains(&self) {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
union.elements(db).iter().when_any(db, |&elem_ty| {
|
||||
self.has_relation_to_impl(
|
||||
db,
|
||||
elem_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B,
|
||||
@@ -3224,8 +3261,22 @@ impl<'db> Type<'db> {
|
||||
/// This function aims to have no false positives, but might return wrong
|
||||
/// `false` answers in some cases.
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
self.when_disjoint_from(db, other, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
#[salsa::tracked(cycle_initial=is_disjoint_from_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn is_disjoint_from_cached<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_ty: Type<'db>,
|
||||
other: Type<'db>,
|
||||
) -> bool {
|
||||
self_ty
|
||||
.when_disjoint_from(db, other, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
}
|
||||
|
||||
if self == other {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_disjoint_from_cached(db, self, other)
|
||||
}
|
||||
|
||||
fn when_disjoint_from(
|
||||
@@ -3307,11 +3358,6 @@ impl<'db> Type<'db> {
|
||||
})
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement disjointness for TypedDict
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are.
|
||||
(Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_))
|
||||
| (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of))
|
||||
@@ -4038,6 +4084,34 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
(Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => ConstraintSet::from(true),
|
||||
|
||||
(Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => {
|
||||
disjointness_visitor.visit((self, other), || {
|
||||
self_typeddict.is_disjoint_from_impl(
|
||||
db,
|
||||
other_typeddict,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// For any type `T`, if `dict[str, Any]` is not assignable to `T`, then all `TypedDict`
|
||||
// types will always be disjoint from `T`. This doesn't cover all cases -- in fact
|
||||
// `dict` *itself* is almost always disjoint from `TypedDict` -- but it's a good
|
||||
// approximation, and some false negatives are acceptable.
|
||||
(Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => KnownClass::Dict
|
||||
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()])
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
other,
|
||||
inferable,
|
||||
TypeRelation::Assignability,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.negate(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8648,6 +8722,37 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_subtype_of_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_id: salsa::Id,
|
||||
_self_ty: Type<'db>,
|
||||
_target: Type<'db>,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_assignable_to_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_id: salsa::Id,
|
||||
_self_ty: Type<'db>,
|
||||
_target: Type<'db>,
|
||||
) -> bool {
|
||||
// In case of a cycle, conservatively assume assignable to avoid false positives
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_disjoint_from_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_id: salsa::Id,
|
||||
_self_ty: Type<'db>,
|
||||
_other: Type<'db>,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_redundant_with_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
|
||||
@@ -365,7 +365,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
Type::StringLiteral(literal) => {
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
let mut ty_negated = None;
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::StringLiterals(literals) => {
|
||||
@@ -383,8 +383,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
|
||||
if negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
self.collapse_to_object();
|
||||
@@ -410,7 +412,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
Type::BytesLiteral(literal) => {
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
let mut ty_negated = None;
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::BytesLiterals(literals) => {
|
||||
@@ -428,8 +430,11 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
|
||||
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
|
||||
if negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
self.collapse_to_object();
|
||||
@@ -455,7 +460,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
Type::IntLiteral(literal) => {
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
let ty_negated = ty.negate(self.db);
|
||||
let mut ty_negated = None;
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
@@ -473,8 +478,11 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
if ty_negated.is_subtype_of(self.db, *existing) {
|
||||
|
||||
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
|
||||
if negated.is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
self.collapse_to_object();
|
||||
@@ -549,19 +557,28 @@ impl<'db> UnionBuilder<'db> {
|
||||
// unpacking them.
|
||||
let should_simplify_full = !matches!(ty, Type::TypeAlias(_)) && !self.cycle_recovery;
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
let ty_negated = if should_simplify_full {
|
||||
ty.negate(self.db)
|
||||
} else {
|
||||
Type::Never // won't be used
|
||||
let mut ty_negated: Option<Type> = None;
|
||||
|
||||
let mut i = 0;
|
||||
let mut insertion_point: Option<usize> = None;
|
||||
|
||||
let mut remove_or_replace = |i: usize, elements: &mut Vec<UnionElement<'db>>| {
|
||||
if insertion_point.is_none() {
|
||||
insertion_point = Some(i);
|
||||
} else {
|
||||
elements.swap_remove(i);
|
||||
}
|
||||
};
|
||||
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
while i < self.elements.len() {
|
||||
let element = &mut self.elements[i];
|
||||
|
||||
let element_type = match element.try_reduce(self.db, ty) {
|
||||
ReduceResult::KeepIf(keep) => {
|
||||
if !keep {
|
||||
to_remove.push(index);
|
||||
remove_or_replace(i, &mut self.elements);
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
ReduceResult::Type(ty) => ty,
|
||||
@@ -587,19 +604,24 @@ impl<'db> UnionBuilder<'db> {
|
||||
// problematic if some of those fields point to recursive `Union`s. To avoid cycles,
|
||||
// compare `TypedDict`s by name/identity instead of using the `has_relation_to`
|
||||
// machinery.
|
||||
if let (Type::TypedDict(element_td), Type::TypedDict(ty_td)) = (element_type, ty) {
|
||||
if element_td == ty_td {
|
||||
return;
|
||||
}
|
||||
if element_type.is_typed_dict() && ty.is_typed_dict() {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) {
|
||||
if ty.is_redundant_with(self.db, element_type) {
|
||||
return;
|
||||
} else if element_type.is_redundant_with(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
} else if ty_negated.is_subtype_of(self.db, element_type) {
|
||||
}
|
||||
|
||||
if element_type.is_redundant_with(self.db, ty) {
|
||||
remove_or_replace(i, &mut self.elements);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
|
||||
if negated.is_subtype_of(self.db, element_type) {
|
||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an
|
||||
// existing `element`. This also means that `~ty | ty` is a subtype of
|
||||
// `element | ty`, because both elements in the first union are subtypes of
|
||||
@@ -613,13 +635,12 @@ impl<'db> UnionBuilder<'db> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
if let Some((&first, rest)) = to_remove.split_first() {
|
||||
self.elements[first] = UnionElement::Type(ty);
|
||||
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
|
||||
for &index in rest.iter().rev() {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
|
||||
if let Some(insertion_point) = insertion_point {
|
||||
self.elements[insertion_point] = UnionElement::Type(ty);
|
||||
} else {
|
||||
self.elements.push(UnionElement::Type(ty));
|
||||
}
|
||||
|
||||
@@ -600,8 +600,22 @@ impl<'db> ClassType<'db> {
|
||||
|
||||
/// Return `true` if `other` is present in this class's MRO.
|
||||
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
self.when_subclass_of(db, other, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
#[salsa::tracked(cycle_initial=is_subclass_of_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn is_subclass_of_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_ty: ClassType<'db>,
|
||||
other: ClassType<'db>,
|
||||
) -> bool {
|
||||
self_ty
|
||||
.when_subclass_of(db, other, InferableTypeVars::None)
|
||||
.is_always_satisfied(db)
|
||||
}
|
||||
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
is_subclass_of_impl(db, self, other)
|
||||
}
|
||||
|
||||
pub(super) fn when_subclass_of(
|
||||
@@ -714,6 +728,7 @@ impl<'db> ClassType<'db> {
|
||||
/// Return the [`DisjointBase`] that appears first in the MRO of this class.
|
||||
///
|
||||
/// Returns `None` if this class does not have any disjoint bases in its MRO.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
pub(super) fn nearest_disjoint_base(self, db: &'db dyn Db) -> Option<DisjointBase<'db>> {
|
||||
self.iter_mro(db)
|
||||
.filter_map(ClassBase::into_class)
|
||||
@@ -1360,6 +1375,16 @@ fn into_callable_cycle_initial<'db>(
|
||||
CallableTypes::one(CallableType::bottom(db))
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_subclass_of_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_id: salsa::Id,
|
||||
_self_ty: ClassType<'db>,
|
||||
_other: ClassType<'db>,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl<'db> From<GenericAlias<'db>> for ClassType<'db> {
|
||||
fn from(generic: GenericAlias<'db>) -> ClassType<'db> {
|
||||
ClassType::Generic(generic)
|
||||
@@ -4110,7 +4135,7 @@ impl InheritanceCycle {
|
||||
/// `TypeError`s resulting from class definitions.
|
||||
///
|
||||
/// [PEP 800]: https://peps.python.org/pep-0800/
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, get_size2::GetSize, salsa::Update)]
|
||||
pub(super) struct DisjointBase<'db> {
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) kind: DisjointBaseKind,
|
||||
@@ -4147,7 +4172,7 @@ impl<'db> DisjointBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
|
||||
pub(super) enum DisjointBaseKind {
|
||||
/// We know the class is a disjoint base because it's either hardcoded in ty
|
||||
/// or has the `@disjoint_base` decorator.
|
||||
|
||||
@@ -83,7 +83,7 @@ use crate::types::{
|
||||
BoundTypeVarIdentity, BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints,
|
||||
UnionType, walk_bound_type_var_type,
|
||||
};
|
||||
use crate::{Db, FxOrderMap};
|
||||
use crate::{Db, FxOrderMap, FxOrderSet};
|
||||
|
||||
/// An extension trait for building constraint sets from [`Option`] values.
|
||||
pub(crate) trait OptionConstraintsExtension<T> {
|
||||
@@ -2222,10 +2222,21 @@ impl<'db> InteriorNode<'db> {
|
||||
constraints = %Node::Interior(self).display(db),
|
||||
"create sequent map",
|
||||
);
|
||||
let mut map = SequentMap::default();
|
||||
Node::Interior(self).for_each_constraint(db, &mut |constraint, _| {
|
||||
map.add(db, constraint);
|
||||
|
||||
// Sort the constraints in this BDD by their `source_order`s before adding them to the
|
||||
// sequent map. This ensures that constraints appear in the sequent map in a stable order.
|
||||
// The constraints mentioned in a BDD should all have distinct `source_order`s, so an
|
||||
// unstable sort is fine.
|
||||
let mut constraints = Vec::new();
|
||||
Node::Interior(self).for_each_constraint(db, &mut |constraint, source_order| {
|
||||
constraints.push((constraint, source_order));
|
||||
});
|
||||
constraints.sort_unstable_by_key(|(_, source_order)| *source_order);
|
||||
|
||||
let mut map = SequentMap::default();
|
||||
for (constraint, _) in constraints {
|
||||
map.add(db, constraint);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
@@ -2781,10 +2792,10 @@ struct SequentMap<'db> {
|
||||
/// Sequents of the form `C₁ ∧ C₂ → D`
|
||||
pair_implications: FxHashMap<
|
||||
(ConstrainedTypeVar<'db>, ConstrainedTypeVar<'db>),
|
||||
FxHashSet<ConstrainedTypeVar<'db>>,
|
||||
FxOrderSet<ConstrainedTypeVar<'db>>,
|
||||
>,
|
||||
/// Sequents of the form `C → D`
|
||||
single_implications: FxHashMap<ConstrainedTypeVar<'db>, FxHashSet<ConstrainedTypeVar<'db>>>,
|
||||
single_implications: FxHashMap<ConstrainedTypeVar<'db>, FxOrderSet<ConstrainedTypeVar<'db>>>,
|
||||
/// Constraints that we have already processed
|
||||
processed: FxHashSet<ConstrainedTypeVar<'db>>,
|
||||
/// Constraints that enqueued to be processed
|
||||
|
||||
@@ -1594,7 +1594,7 @@ declare_lint! {
|
||||
pub(crate) static POSSIBLY_MISSING_IMPORT = {
|
||||
summary: "detects possibly missing imports",
|
||||
status: LintStatus::stable("0.0.1-alpha.22"),
|
||||
default_level: Level::Warn,
|
||||
default_level: Level::Ignore,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1590,14 +1590,19 @@ impl<'db> SpecializationBuilder<'db> {
|
||||
upper: FxOrderSet<Type<'db>>,
|
||||
}
|
||||
|
||||
// Sort the constraints in each path by their `source_order`s, to ensure that we construct
|
||||
// any unions or intersections in our type mappings in a stable order. Constraints might
|
||||
// come out of `PathAssignment`s with identical `source_order`s, but if they do, those
|
||||
// "tied" constraints will still be ordered in a stable way. So we need a stable sort to
|
||||
// retain that stable per-tie ordering.
|
||||
let constraints = constraints.limit_to_valid_specializations(self.db);
|
||||
let mut sorted_paths = Vec::new();
|
||||
constraints.for_each_path(self.db, |path| {
|
||||
let mut path: Vec<_> = path.positive_constraints().collect();
|
||||
path.sort_unstable_by_key(|(_, source_order)| *source_order);
|
||||
path.sort_by_key(|(_, source_order)| *source_order);
|
||||
sorted_paths.push(path);
|
||||
});
|
||||
sorted_paths.sort_unstable_by(|path1, path2| {
|
||||
sorted_paths.sort_by(|path1, path2| {
|
||||
let source_orders1 = path1.iter().map(|(_, source_order)| *source_order);
|
||||
let source_orders2 = path2.iter().map(|(_, source_order)| *source_order);
|
||||
source_orders1.cmp(source_orders2)
|
||||
|
||||
@@ -310,6 +310,14 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
|
||||
/// A list of `dataclass_transform` field specifiers that are "active" (when inferring
|
||||
/// the right hand side of an annotated assignment in a class that is a dataclass).
|
||||
dataclass_field_specifiers: SmallVec<[Type<'db>; NUM_FIELD_SPECIFIERS_INLINE]>,
|
||||
|
||||
/// Unions we're currently narrowing against in ancestor calls.
|
||||
///
|
||||
/// When inferring a call expression with a union type context, we try narrowing to each
|
||||
/// element of the union. If nested calls have the same union as their parameter type,
|
||||
/// this would lead to exponential blowup. By tracking which unions we're already narrowing
|
||||
/// against, we skip redundant nested narrowing.
|
||||
narrowing_unions: FxHashSet<UnionType<'db>>,
|
||||
}
|
||||
|
||||
impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
@@ -348,6 +356,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
cycle_recovery: None,
|
||||
all_definitely_bound: true,
|
||||
dataclass_field_specifiers: SmallVec::new(),
|
||||
narrowing_unions: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7008,13 +7017,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let db = self.db();
|
||||
|
||||
// If the type context is a union, attempt to narrow to a specific element.
|
||||
let narrow_targets: &[_] = match call_expression_tcx.annotation {
|
||||
// TODO: We could theoretically attempt to narrow to every element of
|
||||
// the power set of this union. However, this leads to an exponential
|
||||
// explosion of inference attempts, and is rarely needed in practice.
|
||||
Some(Type::Union(union)) => union.elements(db),
|
||||
_ => &[],
|
||||
};
|
||||
// However, skip narrowing if we're already narrowing against the same union
|
||||
// in an ancestor call to avoid exponential blowup with deeply nested calls.
|
||||
let (narrow_union, narrow_targets): (Option<UnionType<'db>>, &[_]) =
|
||||
match call_expression_tcx.annotation {
|
||||
Some(Type::Union(union)) if !self.narrowing_unions.contains(&union) => {
|
||||
// TODO: We could theoretically attempt to narrow to every element of
|
||||
// the power set of this union. However, this leads to an exponential
|
||||
// explosion of inference attempts, and is rarely needed in practice.
|
||||
(Some(union), union.elements(db))
|
||||
}
|
||||
_ => (None, &[]),
|
||||
};
|
||||
|
||||
// Track that we're narrowing against this union to prevent nested calls
|
||||
// from redundantly narrowing against the same union.
|
||||
if let Some(union) = narrow_union {
|
||||
self.narrowing_unions.insert(union);
|
||||
}
|
||||
|
||||
// We silence diagnostics until we successfully narrow to a specific type.
|
||||
let mut speculated_bindings = bindings.clone();
|
||||
@@ -7082,12 +7102,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
};
|
||||
|
||||
// Prefer the declared type of generic classes.
|
||||
let mut narrowing_result = None;
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.filter(|ty| ty.class_specialization(db).is_some())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
return result;
|
||||
narrowing_result = Some(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7095,15 +7117,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
//
|
||||
// TODO: We could also attempt an inference without type context, but this
|
||||
// leads to similar performance issues.
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.filter(|ty| ty.class_specialization(db).is_none())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
return result;
|
||||
if narrowing_result.is_none() {
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.filter(|ty| ty.class_specialization(db).is_none())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
narrowing_result = Some(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up: remove the union from the tracking set.
|
||||
if let Some(union) = narrow_union {
|
||||
self.narrowing_unions.remove(&union);
|
||||
}
|
||||
|
||||
// If narrowing succeeded, return the result.
|
||||
if let Some(result) = narrowing_result {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Re-enable diagnostics, and infer against the entire union as a fallback.
|
||||
self.context.set_multi_inference(was_in_multi_inference);
|
||||
|
||||
@@ -12592,6 +12627,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
index: _,
|
||||
region: _,
|
||||
return_types_and_ranges: _,
|
||||
narrowing_unions: _,
|
||||
} = self;
|
||||
|
||||
let diagnostics = context.finish();
|
||||
@@ -12659,6 +12695,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
index: _,
|
||||
region: _,
|
||||
return_types_and_ranges: _,
|
||||
narrowing_unions: _,
|
||||
} = self;
|
||||
|
||||
let _ = scope;
|
||||
@@ -12736,6 +12773,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
index: _,
|
||||
region: _,
|
||||
return_types_and_ranges: _,
|
||||
narrowing_unions: _,
|
||||
} = self;
|
||||
|
||||
let _ = scope;
|
||||
|
||||
@@ -720,7 +720,7 @@ impl<'db> ProtocolInstanceType<'db> {
|
||||
_value: ProtocolInstanceType<'db>,
|
||||
_: (),
|
||||
) -> bool {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
is_equivalent_to_object_inner(db, self, ())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
@@ -341,6 +342,148 @@ impl<'db> TypedDictType<'db> {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Two `TypedDict`s `A` and `B` are disjoint if it's impossible to come up with a third
|
||||
/// `TypedDict` `C` that's fully-static and assignable to both of them.
|
||||
///
|
||||
/// `TypedDict` assignability is determined field-by-field, so we determine disjointness
|
||||
/// similarly. For any field that's only in `A`, it's always possible for our hypothetical `C`
|
||||
/// to copy/paste that field without losing assignability to `B` (and vice versa), so we only
|
||||
/// need to consider fields that are present in both `A` and `B`.
|
||||
///
|
||||
/// There are three properties of each field to consider: the declared type, whether it's
|
||||
/// mutable ("mut" vs "imm" below), and whether it's required ("req" vs "opt" below). Here's a
|
||||
/// table summary of the restrictions on the declared type of a source field (for us that means
|
||||
/// in `C`, which we want to be assignable to both `A` and `B`) given a destination field (for
|
||||
/// us that means in either `A` or `B`). For completeness we'll also include the possibility
|
||||
/// that the source field is missing entirely, though we'll soon see that we can ignore that
|
||||
/// case. This table is essentially what `has_relation_to_impl` implements above. Here
|
||||
/// "equivalent" means the source and destination types must be equivalent/compatible,
|
||||
/// "assignable" means the source must be assignable to the destination, and "-" means the
|
||||
/// assignment is never allowed:
|
||||
///
|
||||
/// | dest ↓ source → | mut + req | mut + opt | imm + req | imm + opt | \[missing] |
|
||||
/// |------------------|------------|------------|------------|------------|---------------|
|
||||
/// | mut + req | equivalent | - | - | - | - |
|
||||
/// | mut + opt | - | equivalent | - | - | - |
|
||||
/// | imm + req | assignable | - | assignable | - | - |
|
||||
/// | imm + opt | assignable | assignable | assignable | assignable | \[dest is obj]|
|
||||
///
|
||||
/// We can cut that table down substantially by noticing two things:
|
||||
///
|
||||
/// - We don't need to consider the cases where the source field (in `C`) is `ReadOnly`/"imm",
|
||||
/// because the mutable version of the same field is always "strictly more assignable". In
|
||||
/// other words, nothing in the `TypedDict` assignability rules ever requires a source field
|
||||
/// to be immutable.
|
||||
/// - We don't need to consider the special case where the source field is missing, because
|
||||
/// that's only allowed when the destination is `ReadOnly[NotRequired[object]]`, which is
|
||||
/// compatible with *any* choice of source field.
|
||||
///
|
||||
/// The cases we actually need to reason about are this smaller table:
|
||||
///
|
||||
/// | dest ↓ source → | mut + req | mut + opt |
|
||||
/// |------------------|------------|------------|
|
||||
/// | mut + req | equivalent | - |
|
||||
/// | mut + opt | - | equivalent |
|
||||
/// | imm + req | assignable | - |
|
||||
/// | imm + opt | assignable | assignable |
|
||||
///
|
||||
/// So, given a field name that's in both `A` and `B`, here are the conditions where it's
|
||||
/// *impossible* to choose a source field for `C` that's compatible with both destinations,
|
||||
/// which tells us that `A` and `B` are disjoint:
|
||||
///
|
||||
/// 1. If one side is "mut+opt" (which forces the field in `C` to be "opt") and the other side
|
||||
/// is "req" (which forces the field in `C` to be "req").
|
||||
/// 2. If both sides are mutable, and their types are not equivalent/compatible. (Because the
|
||||
/// type in `C` must be compatible with both of them.)
|
||||
/// 3. If one sides is mutable, and its type is not assignable to the immutable side's type.
|
||||
/// (Because the type in `C` must be compatible with the mutable side.)
|
||||
/// 4. If both sides are immutable, and their types are disjoint. (Because the type in `C` must
|
||||
/// be assignable to both.)
|
||||
///
|
||||
/// TODO: Adding support for `closed` and `extra_items` will complicate this.
|
||||
pub(crate) fn is_disjoint_from_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: TypedDictType<'db>,
|
||||
inferable: InferableTypeVars<'_, 'db>,
|
||||
disjointness_visitor: &IsDisjointVisitor<'db>,
|
||||
relation_visitor: &HasRelationToVisitor<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
let fields_in_common = btreemap_values_with_same_key(self.items(db), other.items(db));
|
||||
fields_in_common.when_any(db, |(self_field, other_field)| {
|
||||
// Condition 1 above.
|
||||
if self_field.is_required() || other_field.is_required() {
|
||||
if (!self_field.is_required() && !self_field.is_read_only())
|
||||
|| (!other_field.is_required() && !other_field.is_read_only())
|
||||
{
|
||||
// One side demands a `Required` source field, while the other side demands a
|
||||
// `NotRequired` one. They must be disjoint.
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
}
|
||||
if !self_field.is_read_only() && !other_field.is_read_only() {
|
||||
// Condition 2 above. This field is mutable on both sides, so the so the types must
|
||||
// be compatible, i.e. mutually assignable.
|
||||
self_field
|
||||
.declared_ty
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
other_field.declared_ty,
|
||||
inferable,
|
||||
TypeRelation::Assignability,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.and(db, || {
|
||||
other_field.declared_ty.has_relation_to_impl(
|
||||
db,
|
||||
self_field.declared_ty,
|
||||
inferable,
|
||||
TypeRelation::Assignability,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
})
|
||||
.negate(db)
|
||||
} else if !self_field.is_read_only() {
|
||||
// Half of condition 3 above.
|
||||
self_field
|
||||
.declared_ty
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
other_field.declared_ty,
|
||||
inferable,
|
||||
TypeRelation::Assignability,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.negate(db)
|
||||
} else if !other_field.is_read_only() {
|
||||
// The other half of condition 3 above.
|
||||
other_field
|
||||
.declared_ty
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
self_field.declared_ty,
|
||||
inferable,
|
||||
TypeRelation::Assignability,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.negate(db)
|
||||
} else {
|
||||
// Condition 4 above.
|
||||
self_field.declared_ty.is_disjoint_from_impl(
|
||||
db,
|
||||
other_field.declared_ty,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
@@ -905,3 +1048,71 @@ bitflags! {
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for TypedDictFieldFlags {}
|
||||
|
||||
/// Yield all the key/val pairs where the same key is present in both `BTreeMap`s. Take advantage
|
||||
/// of the fact that keys are sorted to walk through each map once without doing any lookups. It
|
||||
/// would be nice if `BTreeMap` had something like `BTreeSet::intersection` that did this for us,
|
||||
/// but as far as I know we have to do it ourselves. Life is hard.
|
||||
fn btreemap_values_with_same_key<'a, K, V1, V2>(
|
||||
left: &'a BTreeMap<K, V1>,
|
||||
right: &'a BTreeMap<K, V2>,
|
||||
) -> impl Iterator<Item = (&'a V1, &'a V2)>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
let mut left_items = left.iter().peekable();
|
||||
let mut right_items = right.iter().peekable();
|
||||
std::iter::from_fn(move || {
|
||||
while let (Some((left_key, left_val)), Some((right_key, right_val))) =
|
||||
(left_items.peek().copied(), right_items.peek().copied())
|
||||
{
|
||||
match left_key.cmp(right_key) {
|
||||
Ordering::Equal => {
|
||||
// Matching keys. Yield this pair of values and advance both iterators.
|
||||
left_items.next();
|
||||
right_items.next();
|
||||
return Some((left_val, right_val));
|
||||
}
|
||||
Ordering::Less => {
|
||||
// `left_items` is behind `right_items` in key order. Advance `left_items`.
|
||||
left_items.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
// The opposite.
|
||||
right_items.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
// We've exhausted one or both of the maps, so there can be no more matching keys.
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_btreemap_overlapping_items() {
|
||||
// A case with partial overlap and gaps.
|
||||
let left = BTreeMap::from_iter([("a", 1), ("b", 2), ("c", 3), ("d", 4), ("e", 5)]);
|
||||
let right = BTreeMap::from_iter([("b", 2.0), ("d", 4.0), ("f", 6.0)]);
|
||||
assert_eq!(
|
||||
btreemap_values_with_same_key(&left, &right).collect::<Vec<_>>(),
|
||||
vec![(&2, &2.0), (&4, &4.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
btreemap_values_with_same_key(&right, &left).collect::<Vec<_>>(),
|
||||
vec![(&2.0, &2), (&4.0, &4)],
|
||||
);
|
||||
|
||||
// A case where one side is empty.
|
||||
let left = BTreeMap::<i32, i32>::new();
|
||||
let right = BTreeMap::<i32, i32>::from_iter([(1, 1), (2, 2)]);
|
||||
assert!(
|
||||
btreemap_values_with_same_key(&left, &right)
|
||||
.next()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
btreemap_values_with_same_key(&right, &left)
|
||||
.next()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ Settings: Settings {
|
||||
"positional-only-parameter-as-kwarg": Error (Default),
|
||||
"possibly-missing-attribute": Warning (Default),
|
||||
"possibly-missing-implicit-call": Warning (Default),
|
||||
"possibly-missing-import": Warning (Default),
|
||||
"raw-string-type-annotation": Error (Default),
|
||||
"redundant-cast": Warning (Default),
|
||||
"static-assert-error": Error (Default),
|
||||
|
||||
@@ -384,6 +384,9 @@ foo()
|
||||
|
||||
It is strongly suggested to use explicit range suppressions, in order to prevent
|
||||
accidental suppressions of violations, especially at global module scope.
|
||||
For this reason, a `RUF104` diagnostic will also be produced for any implicit range.
|
||||
If implicit range suppressions are desired, the `RUF104` rule can be disabled,
|
||||
or an inline `noqa` suppression can be added to the end of the "disable" comment.
|
||||
|
||||
Range suppressions cannot be used to enable or select rules that aren't already
|
||||
selected by the project configuration or runtime flags. An "enable" comment can only
|
||||
|
||||
2
ruff.schema.json
generated
2
ruff.schema.json
generated
@@ -4051,6 +4051,8 @@
|
||||
"RUF100",
|
||||
"RUF101",
|
||||
"RUF102",
|
||||
"RUF103",
|
||||
"RUF104",
|
||||
"RUF2",
|
||||
"RUF20",
|
||||
"RUF200",
|
||||
|
||||
2
ty.schema.json
generated
2
ty.schema.json
generated
@@ -926,7 +926,7 @@
|
||||
"possibly-missing-import": {
|
||||
"title": "detects possibly missing imports",
|
||||
"description": "## What it does\nChecks for imports of symbols that may be missing.\n\n## Why is this bad?\nImporting a missing module or name will raise a `ModuleNotFoundError`\nor `ImportError` at runtime.\n\n## Examples\n```python\n# module.py\nimport datetime\n\nif datetime.date.today().weekday() != 6:\n a = 1\n\n# main.py\nfrom module import a # ImportError: cannot import name 'a' from 'module'\n```",
|
||||
"default": "warn",
|
||||
"default": "ignore",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Level"
|
||||
|
||||
Reference in New Issue
Block a user