Compare commits
24 Commits
dylan/stab
...
brent/stab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ae45f3687 | ||
|
|
d4d29858b4 | ||
|
|
829acf498d | ||
|
|
e07f352f99 | ||
|
|
8d0b6882b7 | ||
|
|
65a2daea02 | ||
|
|
8baaa2f7f3 | ||
|
|
8b1ce32f04 | ||
|
|
eb5abda8ac | ||
|
|
9c4ecf77b6 | ||
|
|
0809d88ca0 | ||
|
|
5c59167686 | ||
|
|
e2ea301c74 | ||
|
|
62364ea47e | ||
|
|
331821244b | ||
|
|
1dc8f8f903 | ||
|
|
301b9f4135 | ||
|
|
86e5a311f0 | ||
|
|
0c20010bb9 | ||
|
|
72552f31e4 | ||
|
|
95497ffaab | ||
|
|
b3b900dc1e | ||
|
|
503427855d | ||
|
|
6e785867c3 |
@@ -5436,14 +5436,15 @@ match 2:
|
||||
print("it's one")
|
||||
"#
|
||||
),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
"###
|
||||
);
|
||||
|
||||
// syntax error on 3.9 with preview
|
||||
|
||||
@@ -171,6 +171,21 @@ pub trait System: Debug {
|
||||
PatternError,
|
||||
>;
|
||||
|
||||
/// Fetches the environment variable `key` from the current process.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`std::env::VarError::NotPresent`] if:
|
||||
/// - The variable is not set.
|
||||
/// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`).
|
||||
///
|
||||
/// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid
|
||||
/// Unicode.
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
let _ = name;
|
||||
Err(std::env::VarError::NotPresent)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
|
||||
@@ -214,6 +214,10 @@ impl System for OsSystem {
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
std::env::var(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl OsSystem {
|
||||
|
||||
@@ -102,3 +102,6 @@ with open("furb129.py") as f:
|
||||
pass
|
||||
for line in(f).readlines():
|
||||
pass
|
||||
|
||||
# Test case for issue #17683 (missing space before keyword)
|
||||
print([line for line in f.readlines()if True])
|
||||
|
||||
@@ -65,7 +65,7 @@ use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::importer::{ImportRequest, Importer, ResolutionError};
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::{is_semantic_errors_enabled, is_undefined_export_in_dunder_init_enabled};
|
||||
use crate::preview::is_undefined_export_in_dunder_init_enabled;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pyflakes::rules::{
|
||||
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
||||
@@ -663,9 +663,7 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
if is_semantic_errors_enabled(self.settings) {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::fix::edits::delete_comment;
|
||||
use crate::noqa::{
|
||||
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
|
||||
};
|
||||
use crate::preview::is_check_file_level_directives_enabled;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::pygrep_hooks;
|
||||
@@ -112,25 +111,16 @@ pub(crate) fn check_noqa(
|
||||
&& !exemption.includes(Rule::UnusedNOQA)
|
||||
&& !per_file_ignores.contains(Rule::UnusedNOQA)
|
||||
{
|
||||
let directives: Vec<_> = if is_check_file_level_directives_enabled(settings) {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.chain(
|
||||
file_noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
|
||||
)
|
||||
.collect()
|
||||
} else {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.collect()
|
||||
};
|
||||
let directives = noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.chain(
|
||||
file_noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
|
||||
);
|
||||
for (directive, matches, is_file_level) in directives {
|
||||
match directive {
|
||||
Directive::All(directive) => {
|
||||
|
||||
@@ -552,7 +552,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional),
|
||||
(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, "049") => (RuleGroup::Stable, rules::pyupgrade::rules::PrivateTypeParameter),
|
||||
(Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType),
|
||||
|
||||
// pydocstyle
|
||||
@@ -1019,7 +1019,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
|
||||
(Ruff, "051") => (RuleGroup::Stable, rules::ruff::rules::IfKeyInDictDel),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "053") => (RuleGroup::Stable, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
|
||||
@@ -1129,7 +1129,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset),
|
||||
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
|
||||
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
|
||||
(Refurb, "162") => (RuleGroup::Preview, rules::refurb::rules::FromisoformatReplaceZ),
|
||||
(Refurb, "162") => (RuleGroup::Stable, rules::refurb::rules::FromisoformatReplaceZ),
|
||||
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
|
||||
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
|
||||
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),
|
||||
@@ -1148,7 +1148,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
// flake8-logging
|
||||
(Flake8Logging, "001") => (RuleGroup::Stable, rules::flake8_logging::rules::DirectLoggerInstantiation),
|
||||
(Flake8Logging, "002") => (RuleGroup::Stable, rules::flake8_logging::rules::InvalidGetLoggerArgument),
|
||||
(Flake8Logging, "004") => (RuleGroup::Preview, rules::flake8_logging::rules::LogExceptionOutsideExceptHandler),
|
||||
(Flake8Logging, "004") => (RuleGroup::Stable, rules::flake8_logging::rules::LogExceptionOutsideExceptHandler),
|
||||
(Flake8Logging, "007") => (RuleGroup::Stable, rules::flake8_logging::rules::ExceptionWithoutExcInfo),
|
||||
(Flake8Logging, "009") => (RuleGroup::Stable, rules::flake8_logging::rules::UndocumentedWarn),
|
||||
(Flake8Logging, "014") => (RuleGroup::Preview, rules::flake8_logging::rules::ExcInfoOutsideExceptHandler),
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::fix::{FixResult, fix_file};
|
||||
use crate::message::Message;
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::{is_py314_support_enabled, is_unsupported_syntax_enabled};
|
||||
use crate::preview::is_py314_support_enabled;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
|
||||
@@ -447,11 +447,7 @@ pub fn check_path(
|
||||
}
|
||||
}
|
||||
|
||||
let syntax_errors = if is_unsupported_syntax_enabled(settings) {
|
||||
parsed.unsupported_syntax_errors()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let syntax_errors = parsed.unsupported_syntax_errors();
|
||||
|
||||
diagnostics_to_messages(
|
||||
diagnostics,
|
||||
|
||||
@@ -7,17 +7,6 @@
|
||||
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
// https://github.com/astral-sh/ruff/issues/17412
|
||||
// https://github.com/astral-sh/ruff/issues/11934
|
||||
pub(crate) const fn is_semantic_errors_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16429
|
||||
pub(crate) const fn is_unsupported_syntax_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
@@ -29,23 +18,11 @@ pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &Linter
|
||||
|
||||
// Rule-specific behavior
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17136
|
||||
pub(crate) const fn is_shell_injection_only_trusted_input_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/15541
|
||||
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/7501
|
||||
pub(crate) const fn is_bool_subtype_of_annotation_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/10759
|
||||
pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -63,21 +40,11 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/12676
|
||||
pub(crate) const fn is_fix_future_annotations_in_stub_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11074
|
||||
pub(crate) const fn is_only_add_return_none_at_end_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/12796
|
||||
pub(crate) const fn is_simplify_ternary_to_binary_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16719
|
||||
pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -104,13 +71,6 @@ pub(crate) const fn is_unicode_to_unicode_confusables_enabled(settings: &LinterS
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17078
|
||||
pub(crate) const fn is_support_slices_in_literal_concatenation_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11370
|
||||
pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -121,16 +81,9 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17061
|
||||
pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/17644
|
||||
pub(crate) const fn is_readlines_in_for_fix_safe_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) const fn multiple_with_statements_fix_safe_enabled(settings: &LinterSettings) -> bool {
|
||||
// https://github.com/astral-sh/ruff/pull/18208
|
||||
pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -104,7 +104,6 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -7,7 +7,6 @@ use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::preview::is_shell_injection_only_trusted_input_enabled;
|
||||
use crate::{
|
||||
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
|
||||
};
|
||||
@@ -325,9 +324,7 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) {
|
||||
}
|
||||
// S603
|
||||
_ => {
|
||||
if !is_trusted_input(arg)
|
||||
|| !is_shell_injection_only_trusted_input_enabled(checker.settings)
|
||||
{
|
||||
if !is_trusted_input(arg) {
|
||||
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
|
||||
checker.report_diagnostic(
|
||||
SubprocessWithoutShellEqualsTrue,
|
||||
|
||||
@@ -106,74 +106,6 @@ S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
23 | # Literals are fine, they're trusted.
|
||||
|
|
||||
|
||||
S603.py:24:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
23 | # Literals are fine, they're trusted.
|
||||
24 | run("true")
|
||||
| ^^^ S603
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:25:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
23 | # Literals are fine, they're trusted.
|
||||
24 | run("true")
|
||||
25 | Popen(["true"])
|
||||
| ^^^^^ S603
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:26:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
24 | run("true")
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
| ^^^^^ S603
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:27:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
25 | Popen(["true"])
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
| ^^^^ S603
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:28:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
26 | Popen("true", shell=False)
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
| ^^^^^^^^^^ S603
|
||||
29 | check_output("true", shell=False)
|
||||
30 | run("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:29:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
27 | call("true", shell=False)
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
30 | run("true", shell=False)
|
||||
|
|
||||
|
||||
S603.py:30:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
28 | check_call("true", shell=False)
|
||||
29 | check_output("true", shell=False)
|
||||
30 | run("true", shell=False)
|
||||
| ^^^ S603
|
||||
31 |
|
||||
32 | # Not through assignments though.
|
||||
|
|
||||
|
||||
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
32 | # Not through assignments though.
|
||||
@@ -184,15 +116,6 @@ S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
36 | # Instant named expressions are fine.
|
||||
|
|
||||
|
||||
S603.py:37:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
36 | # Instant named expressions are fine.
|
||||
37 | run(c := "true")
|
||||
| ^^^ S603
|
||||
38 |
|
||||
39 | # But non-instant are not.
|
||||
|
|
||||
|
||||
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
39 | # But non-instant are not.
|
||||
@@ -200,20 +123,3 @@ S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
41 | run(e)
|
||||
| ^^^ S603
|
||||
|
|
||||
|
||||
S603.py:46:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
44 | # https://github.com/astral-sh/ruff/issues/17798
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
|
|
||||
|
||||
S603.py:47:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
45 | # Tuple literals are trusted
|
||||
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
|
||||
47 | Popen(("literal", "cmd", "using", "tuple"))
|
||||
| ^^^^^ S603
|
||||
|
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S603.py:5:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
3 | # Different Popen wrappers are checked.
|
||||
4 | a = input()
|
||||
5 | Popen(a, shell=False)
|
||||
| ^^^^^ S603
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:6:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
4 | a = input()
|
||||
5 | Popen(a, shell=False)
|
||||
6 | call(a, shell=False)
|
||||
| ^^^^ S603
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:7:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
5 | Popen(a, shell=False)
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
| ^^^^^^^^^^ S603
|
||||
8 | check_output(a, shell=False)
|
||||
9 | run(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:8:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
6 | call(a, shell=False)
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
| ^^^^^^^^^^^^ S603
|
||||
9 | run(a, shell=False)
|
||||
|
|
||||
|
||||
S603.py:9:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
7 | check_call(a, shell=False)
|
||||
8 | check_output(a, shell=False)
|
||||
9 | run(a, shell=False)
|
||||
| ^^^ S603
|
||||
10 |
|
||||
11 | # Falsey values are treated as false.
|
||||
|
|
||||
|
||||
S603.py:12:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
11 | # Falsey values are treated as false.
|
||||
12 | Popen(a, shell=0)
|
||||
| ^^^^^ S603
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
|
|
||||
|
||||
S603.py:13:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
11 | # Falsey values are treated as false.
|
||||
12 | Popen(a, shell=0)
|
||||
13 | Popen(a, shell=[])
|
||||
| ^^^^^ S603
|
||||
14 | Popen(a, shell={})
|
||||
15 | Popen(a, shell=None)
|
||||
|
|
||||
|
||||
S603.py:14:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
12 | Popen(a, shell=0)
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
| ^^^^^ S603
|
||||
15 | Popen(a, shell=None)
|
||||
|
|
||||
|
||||
S603.py:15:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
13 | Popen(a, shell=[])
|
||||
14 | Popen(a, shell={})
|
||||
15 | Popen(a, shell=None)
|
||||
| ^^^^^ S603
|
||||
16 |
|
||||
17 | # Unknown values are treated as falsey.
|
||||
|
|
||||
|
||||
S603.py:18:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
17 | # Unknown values are treated as falsey.
|
||||
18 | Popen(a, shell=True if True else False)
|
||||
| ^^^^^ S603
|
||||
19 |
|
||||
20 | # No value is also caught.
|
||||
|
|
||||
|
||||
S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
20 | # No value is also caught.
|
||||
21 | Popen(a)
|
||||
| ^^^^^ S603
|
||||
22 |
|
||||
23 | # Literals are fine, they're trusted.
|
||||
|
|
||||
|
||||
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
32 | # Not through assignments though.
|
||||
33 | cmd = ["true"]
|
||||
34 | run(cmd)
|
||||
| ^^^ S603
|
||||
35 |
|
||||
36 | # Instant named expressions are fine.
|
||||
|
|
||||
|
||||
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
||||
|
|
||||
39 | # But non-instant are not.
|
||||
40 | (e := "echo")
|
||||
41 | run(e)
|
||||
| ^^^ S603
|
||||
|
|
||||
@@ -12,7 +12,6 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -29,24 +28,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_boolean_trap").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_allowed_callable() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -7,12 +7,12 @@ use ruff_python_semantic::analyze::visibility;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_bool_subtype_of_annotation_enabled;
|
||||
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of boolean positional arguments in function definitions,
|
||||
/// as determined by the presence of a `bool` type hint.
|
||||
/// as determined by the presence of a type hint containing `bool` as an
|
||||
/// evident subtype - e.g. `bool`, `bool | int`, `typing.Optional[bool]`, etc.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling a function with boolean positional arguments is confusing as the
|
||||
@@ -30,9 +30,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// Dunder methods that define operators are exempt from this rule, as are
|
||||
/// setters and `@override` definitions.
|
||||
///
|
||||
/// In [preview], this rule will also flag annotations that include boolean
|
||||
/// variants, like `bool | int`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
@@ -96,8 +93,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BooleanTypeHintPositionalArgument;
|
||||
|
||||
@@ -128,14 +123,8 @@ pub(crate) fn boolean_type_hint_positional_argument(
|
||||
let Some(annotation) = parameter.annotation() else {
|
||||
continue;
|
||||
};
|
||||
if is_bool_subtype_of_annotation_enabled(checker.settings) {
|
||||
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if !match_annotation_to_literal_bool(annotation) {
|
||||
continue;
|
||||
}
|
||||
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow Boolean type hints in setters.
|
||||
@@ -161,17 +150,6 @@ pub(crate) fn boolean_type_hint_positional_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`).
|
||||
fn match_annotation_to_literal_bool(annotation: &Expr) -> bool {
|
||||
match annotation {
|
||||
// Ex) `True`
|
||||
Expr::Name(name) => &name.id == "bool",
|
||||
// Ex) `"True"`
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "bool",
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`), or a type hint that
|
||||
/// includes boolean as a variant (e.g., `bool | int`).
|
||||
fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
@@ -89,3 +88,17 @@ FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
|
||||
| ^^^^^ FBT001
|
||||
91 | pass
|
||||
|
|
||||
|
||||
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
| ^ FBT001
|
||||
101 | pass
|
||||
|
|
||||
|
||||
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
104 | def func(x: bool | str):
|
||||
| ^ FBT001
|
||||
105 | pass
|
||||
|
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
2 | posonly_nohint,
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^ FBT001
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
6 | /,
|
||||
|
|
||||
|
||||
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
3 | posonly_nonboolhint: int,
|
||||
4 | posonly_boolhint: bool,
|
||||
5 | posonly_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
6 | /,
|
||||
7 | offset,
|
||||
|
|
||||
|
||||
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
8 | posorkw_nonvalued_nohint,
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
|
|
||||
|
||||
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
9 | posorkw_nonvalued_nonboolhint: int,
|
||||
10 | posorkw_nonvalued_boolhint: bool,
|
||||
11 | posorkw_nonvalued_boolstrhint: "bool",
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
|
|
||||
|
||||
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
12 | posorkw_boolvalued_nohint=True,
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
|
|
||||
|
||||
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
13 | posorkw_boolvalued_nonboolhint: int = True,
|
||||
14 | posorkw_boolvalued_boolhint: bool = True,
|
||||
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
|
|
||||
|
||||
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
16 | posorkw_nonboolvalued_nohint=1,
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
20 | *,
|
||||
|
|
||||
|
||||
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
||||
18 | posorkw_nonboolvalued_boolhint: bool = 3,
|
||||
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
|
||||
20 | *,
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
89 | # FBT001: Boolean positional arg in function definition
|
||||
90 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^ FBT001
|
||||
91 | pass
|
||||
|
|
||||
|
||||
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
|
||||
| ^ FBT001
|
||||
101 | pass
|
||||
|
|
||||
|
||||
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
104 | def func(x: bool | str):
|
||||
| ^ FBT001
|
||||
105 | pass
|
||||
|
|
||||
@@ -11,7 +11,7 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// Checks for `.exception()` logging calls outside of exception handlers.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// [The documentation] states:
|
||||
/// The Python `logging` [documentation] states:
|
||||
/// > This function should only be called from an exception handler.
|
||||
///
|
||||
/// Calling `.exception()` outside of an exception handler
|
||||
@@ -23,6 +23,9 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// NoneType: None
|
||||
/// ```
|
||||
///
|
||||
/// Although this confusion can be avoided by passing an explicit `exc_info` keyword argument, this
|
||||
/// rule will still emit a diagnostic, in line with the `logging` documentation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
@@ -42,7 +45,7 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// ## Fix safety
|
||||
/// The fix, if available, will always be marked as unsafe, as it changes runtime behavior.
|
||||
///
|
||||
/// [The documentation]: https://docs.python.org/3/library/logging.html#logging.exception
|
||||
/// [documentation]: https://docs.python.org/3/library/logging.html#logging.exception
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct LogExceptionOutsideExceptHandler;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pep8_naming;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -172,22 +171,4 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_pyi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_python_ast::StmtImportFrom;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::{Fix, FixAvailability, Violation};
|
||||
use crate::{checkers::ast::Checker, fix, preview::is_fix_future_annotations_in_stub_enabled};
|
||||
use crate::{checkers::ast::Checker, fix};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of the `from __future__ import annotations` import
|
||||
@@ -55,20 +55,18 @@ pub(crate) fn from_future_import(checker: &Checker, target: &StmtImportFrom) {
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(FutureAnnotationsInStub, *range);
|
||||
|
||||
if is_fix_future_annotations_in_stub_enabled(checker.settings) {
|
||||
let stmt = checker.semantic().current_statement();
|
||||
let stmt = checker.semantic().current_statement();
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once("annotations"),
|
||||
stmt,
|
||||
None,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit = fix::edits::remove_unused_imports(
|
||||
std::iter::once("annotations"),
|
||||
stmt,
|
||||
None,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.indexer(),
|
||||
)?;
|
||||
|
||||
Ok(Fix::safe_edit(edit))
|
||||
});
|
||||
}
|
||||
Ok(Fix::safe_edit(edit))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
@@ -10,7 +10,14 @@ PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stu
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 |-from __future__ import annotations # PYI044.
|
||||
3 2 | from __future__ import annotations, with_statement # PYI044.
|
||||
4 3 |
|
||||
5 4 | # Good imports.
|
||||
|
||||
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
@@ -20,3 +27,12 @@ PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stu
|
||||
5 | # Good imports.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 2 | from __future__ import annotations # PYI044.
|
||||
3 |-from __future__ import annotations, with_statement # PYI044.
|
||||
3 |+from __future__ import with_statement # PYI044.
|
||||
4 4 |
|
||||
5 5 | # Good imports.
|
||||
6 6 | from __future__ import with_statement
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
|
||||
3 | from __future__ import annotations, with_statement # PYI044.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 |-from __future__ import annotations # PYI044.
|
||||
3 2 | from __future__ import annotations, with_statement # PYI044.
|
||||
4 3 |
|
||||
5 4 | # Good imports.
|
||||
|
||||
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
||||
|
|
||||
1 | # Bad import.
|
||||
2 | from __future__ import annotations # PYI044.
|
||||
3 | from __future__ import annotations, with_statement # PYI044.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
|
||||
4 |
|
||||
5 | # Good imports.
|
||||
|
|
||||
= help: Remove `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Bad import.
|
||||
2 2 | from __future__ import annotations # PYI044.
|
||||
3 |-from __future__ import annotations, with_statement # PYI044.
|
||||
3 |+from __future__ import with_statement # PYI044.
|
||||
4 4 |
|
||||
5 5 | # Good imports.
|
||||
6 6 | from __future__ import with_statement
|
||||
@@ -58,7 +58,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
|
||||
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
|
||||
@@ -10,7 +10,7 @@ use super::fix_with;
|
||||
use crate::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::fits;
|
||||
use crate::preview::multiple_with_statements_fix_safe_enabled;
|
||||
use crate::preview::is_multiple_with_statements_fix_safe_enabled;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -195,7 +195,7 @@ pub(crate) fn multiple_with_statements(
|
||||
checker.settings.tab_size,
|
||||
)
|
||||
}) {
|
||||
if multiple_with_statements_fix_safe_enabled(checker.settings) {
|
||||
if is_multiple_with_statements_fix_safe_enabled(checker.settings) {
|
||||
Ok(Some(Fix::safe_edit(edit)))
|
||||
} else {
|
||||
Ok(Some(Fix::unsafe_edit(edit)))
|
||||
|
||||
@@ -7,13 +7,14 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::fits;
|
||||
use crate::preview::is_simplify_ternary_to_binary_enabled;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Check for `if`-`else`-blocks that can be replaced with a ternary operator.
|
||||
/// Moreover, in [preview], check if these ternary expressions can be
|
||||
/// further simplified to binary expressions.
|
||||
/// Check for `if`-`else`-blocks that can be replaced with a ternary
|
||||
/// or binary operator.
|
||||
///
|
||||
/// The lint is suppressed if the suggested replacement would exceed
|
||||
/// the maximum line length configured in [pycodestyle.max-line-length].
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `if`-`else`-blocks that assign a value to a variable in both branches can
|
||||
@@ -33,7 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// bar = x if foo else y
|
||||
/// ```
|
||||
///
|
||||
/// Or, in [preview]:
|
||||
/// Or:
|
||||
///
|
||||
/// ```python
|
||||
/// if cond:
|
||||
@@ -57,8 +58,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: Conditional expressions](https://docs.python.org/3/reference/expressions.html#conditional-expressions)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
/// [code coverage]: https://github.com/nedbat/coveragepy/issues/509
|
||||
/// [pycodestyle.max-line-length]: https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-line-length
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct IfElseBlockInsteadOfIfExp {
|
||||
/// The ternary or binary expression to replace the `if`-`else`-block.
|
||||
@@ -183,16 +184,12 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
|
||||
//
|
||||
// The match statement below implements the following
|
||||
// logic:
|
||||
// - If `test == body_value` and preview enabled, replace with `target_var = test or else_value`
|
||||
// - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - If `test == body_value`, replace with `target_var = test or else_value`
|
||||
// - If `test == not body_value`, replace with `target_var = body_value and else_value`
|
||||
// - If `not test == body_value`, replace with `target_var = body_value and else_value`
|
||||
// - Otherwise, replace with `target_var = body_value if test else else_value`
|
||||
let (contents, assignment_kind) = match (
|
||||
is_simplify_ternary_to_binary_enabled(checker.settings),
|
||||
test,
|
||||
body_value,
|
||||
) {
|
||||
(true, test_node, body_node)
|
||||
let (contents, assignment_kind) = match (test, body_value) {
|
||||
(test_node, body_node)
|
||||
if ComparableExpr::from(test_node) == ComparableExpr::from(body_node)
|
||||
&& !contains_effect(test_node, |id| checker.semantic().has_builtin_binding(id)) =>
|
||||
{
|
||||
@@ -200,7 +197,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
|
||||
let binary = assignment_binary_or(target_var, body_value, else_value);
|
||||
(checker.generator().stmt(&binary), AssignmentKind::Binary)
|
||||
}
|
||||
(true, test_node, body_node)
|
||||
(test_node, body_node)
|
||||
if (test_node.as_unary_op_expr().is_some_and(|op_expr| {
|
||||
op_expr.op.is_not()
|
||||
&& ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node)
|
||||
|
||||
@@ -118,7 +118,7 @@ SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
@@ -130,7 +130,7 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = cond or other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
@@ -140,12 +140,12 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond if cond else other_cond
|
||||
141 |+z = cond or other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
@@ -157,7 +157,7 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if not cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
@@ -167,12 +167,12 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond if not cond else other_cond
|
||||
148 |+z = cond and other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else other_cond` instead of `if`-`else`-block
|
||||
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
@@ -184,7 +184,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond if cond else other_cond`
|
||||
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
@@ -194,7 +194,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond if cond else other_cond
|
||||
155 |+z = not cond and other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `if`-`else`-block
|
||||
|
|
||||
1 | # SIM108
|
||||
2 | / if a:
|
||||
3 | | b = c
|
||||
4 | | else:
|
||||
5 | | b = d
|
||||
| |_________^ SIM108
|
||||
6 |
|
||||
7 | # OK
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = c if a else d`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | # SIM108
|
||||
2 |-if a:
|
||||
3 |- b = c
|
||||
4 |-else:
|
||||
5 |- b = d
|
||||
2 |+b = c if a else d
|
||||
6 3 |
|
||||
7 4 | # OK
|
||||
8 5 | b = c if a else d
|
||||
|
||||
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
||||
|
|
||||
28 | pass
|
||||
29 | else:
|
||||
30 | / if a:
|
||||
31 | | b = 1
|
||||
32 | | else:
|
||||
33 | | b = 2
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
27 27 | if True:
|
||||
28 28 | pass
|
||||
29 29 | else:
|
||||
30 |- if a:
|
||||
31 |- b = 1
|
||||
32 |- else:
|
||||
33 |- b = 2
|
||||
30 |+ b = 1 if a else 2
|
||||
34 31 |
|
||||
35 32 |
|
||||
36 33 | import sys
|
||||
|
||||
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
||||
|
|
||||
57 | # SIM108 (without fix due to comments)
|
||||
58 | / if x > 0:
|
||||
59 | | # test test
|
||||
60 | | abc = x
|
||||
61 | | else:
|
||||
62 | | # test test test
|
||||
63 | | abc = -x
|
||||
| |____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `abc = x if x > 0 else -x`
|
||||
|
||||
SIM108.py:82:1: SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block
|
||||
|
|
||||
81 | # SIM108
|
||||
82 | / if a:
|
||||
83 | | b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 | | else:
|
||||
85 | | b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
| |_____________________________________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"`
|
||||
|
||||
ℹ Unsafe fix
|
||||
79 79 |
|
||||
80 80 |
|
||||
81 81 | # SIM108
|
||||
82 |-if a:
|
||||
83 |- b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 |-else:
|
||||
85 |- b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
82 |+b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"
|
||||
86 83 |
|
||||
87 84 |
|
||||
88 85 | # OK (too long)
|
||||
|
||||
SIM108.py:105:1: SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else`-block
|
||||
|
|
||||
104 | # SIM108 (without fix due to trailing comment)
|
||||
105 | / if True:
|
||||
106 | | exitcode = 0
|
||||
107 | | else:
|
||||
108 | | exitcode = 1 # Trailing comment
|
||||
| |________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `exitcode = 0 if True else 1`
|
||||
|
||||
SIM108.py:112:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
111 | # SIM108
|
||||
112 | / if True: x = 3 # Foo
|
||||
113 | | else: x = 5
|
||||
| |___________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
116 | # SIM108
|
||||
117 | / if True: # Foo
|
||||
118 | | x = 3
|
||||
119 | | else:
|
||||
120 | | x = 5
|
||||
| |_________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
141 | / if cond:
|
||||
142 | | z = cond
|
||||
143 | | else:
|
||||
144 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond or other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
139 139 | # SIM108 - should suggest
|
||||
140 140 | # z = cond or other_cond
|
||||
141 |-if cond:
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond or other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
148 | / if not cond:
|
||||
149 | | z = cond
|
||||
150 | | else:
|
||||
151 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
146 146 | # SIM108 - should suggest
|
||||
147 147 | # z = cond and other_cond
|
||||
148 |-if not cond:
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond and other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
155 | / if cond:
|
||||
156 | | z = not cond
|
||||
157 | | else:
|
||||
158 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
153 153 | # SIM108 - should suggest
|
||||
154 154 | # z = not cond and other_cond
|
||||
155 |-if cond:
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond and other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block
|
||||
|
|
||||
165 | # (Of course, these specific expressions
|
||||
166 | # should be simplified for other reasons...)
|
||||
167 | / if True:
|
||||
168 | | z = 1
|
||||
169 | | else:
|
||||
170 | | z = other
|
||||
| |_____________^ SIM108
|
||||
171 |
|
||||
172 | if False:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if True else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
164 164 | # so, e.g. `True == 1`.
|
||||
165 165 | # (Of course, these specific expressions
|
||||
166 166 | # should be simplified for other reasons...)
|
||||
167 |-if True:
|
||||
168 |- z = 1
|
||||
169 |-else:
|
||||
170 |- z = other
|
||||
167 |+z = 1 if True else other
|
||||
171 168 |
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
||||
|
|
||||
170 | z = other
|
||||
171 |
|
||||
172 | / if False:
|
||||
173 | | z = 1
|
||||
174 | | else:
|
||||
175 | | z = other
|
||||
| |_____________^ SIM108
|
||||
176 |
|
||||
177 | if 1:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if False else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
169 169 | else:
|
||||
170 170 | z = other
|
||||
171 171 |
|
||||
172 |-if False:
|
||||
173 |- z = 1
|
||||
174 |-else:
|
||||
175 |- z = other
|
||||
172 |+z = 1 if False else other
|
||||
176 173 |
|
||||
177 174 | if 1:
|
||||
178 175 | z = True
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
176 |
|
||||
177 | / if 1:
|
||||
178 | | z = True
|
||||
179 | | else:
|
||||
180 | | z = other
|
||||
| |_____________^ SIM108
|
||||
181 |
|
||||
182 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = True if 1 else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
174 174 | else:
|
||||
175 175 | z = other
|
||||
176 176 |
|
||||
177 |-if 1:
|
||||
178 |- z = True
|
||||
179 |-else:
|
||||
180 |- z = other
|
||||
177 |+z = True if 1 else other
|
||||
181 178 |
|
||||
182 179 | # SIM108 does not suggest a binary option in this
|
||||
183 180 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
183 | # case, since we'd be reducing the number of calls
|
||||
184 | # from Two to one.
|
||||
185 | / if foo():
|
||||
186 | | z = foo()
|
||||
187 | | else:
|
||||
188 | | z = other
|
||||
| |_____________^ SIM108
|
||||
189 |
|
||||
190 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
182 182 | # SIM108 does not suggest a binary option in this
|
||||
183 183 | # case, since we'd be reducing the number of calls
|
||||
184 184 | # from Two to one.
|
||||
185 |-if foo():
|
||||
186 |- z = foo()
|
||||
187 |-else:
|
||||
188 |- z = other
|
||||
185 |+z = foo() if foo() else other
|
||||
189 186 |
|
||||
190 187 | # SIM108 does not suggest a binary option in this
|
||||
191 188 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
191 | # case, since we'd be reducing the number of calls
|
||||
192 | # from Two to one.
|
||||
193 | / if foo():
|
||||
194 | | z = not foo()
|
||||
195 | | else:
|
||||
196 | | z = other
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
190 190 | # SIM108 does not suggest a binary option in this
|
||||
191 191 | # case, since we'd be reducing the number of calls
|
||||
192 192 | # from Two to one.
|
||||
193 |-if foo():
|
||||
194 |- z = not foo()
|
||||
195 |-else:
|
||||
196 |- z = other
|
||||
193 |+z = not foo() if foo() else other
|
||||
197 194 |
|
||||
198 195 |
|
||||
199 196 | # These two cases double as tests for f-string quote preservation. The first
|
||||
|
||||
SIM108.py:202:1: SIM108 [*] Use ternary operator `var = "str" if cond else f"{first}-{second}"` instead of `if`-`else`-block
|
||||
|
|
||||
200 | # f-string should preserve its double quotes, and the second should preserve
|
||||
201 | # single quotes
|
||||
202 | / if cond:
|
||||
203 | | var = "str"
|
||||
204 | | else:
|
||||
205 | | var = f"{first}-{second}"
|
||||
| |_____________________________^ SIM108
|
||||
206 |
|
||||
207 | if cond:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `var = "str" if cond else f"{first}-{second}"`
|
||||
|
||||
ℹ Unsafe fix
|
||||
199 199 | # These two cases double as tests for f-string quote preservation. The first
|
||||
200 200 | # f-string should preserve its double quotes, and the second should preserve
|
||||
201 201 | # single quotes
|
||||
202 |-if cond:
|
||||
203 |- var = "str"
|
||||
204 |-else:
|
||||
205 |- var = f"{first}-{second}"
|
||||
202 |+var = "str" if cond else f"{first}-{second}"
|
||||
206 203 |
|
||||
207 204 | if cond:
|
||||
208 205 | var = "str"
|
||||
|
||||
SIM108.py:207:1: SIM108 [*] Use ternary operator `var = "str" if cond else f'{first}-{second}'` instead of `if`-`else`-block
|
||||
|
|
||||
205 | var = f"{first}-{second}"
|
||||
206 |
|
||||
207 | / if cond:
|
||||
208 | | var = "str"
|
||||
209 | | else:
|
||||
210 | | var = f'{first}-{second}'
|
||||
| |_____________________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `var = "str" if cond else f'{first}-{second}'`
|
||||
|
||||
ℹ Unsafe fix
|
||||
204 204 | else:
|
||||
205 205 | var = f"{first}-{second}"
|
||||
206 206 |
|
||||
207 |-if cond:
|
||||
208 |- var = "str"
|
||||
209 |-else:
|
||||
210 |- var = f'{first}-{second}'
|
||||
207 |+var = "str" if cond else f'{first}-{second}'
|
||||
@@ -62,24 +62,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::ReadlinesInFor, Path::new("FURB129.py"))]
|
||||
fn preview(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: settings::types::PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_whole_file_python_39() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_python_semantic::analyze::typing::is_io_base_expr;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_readlines_in_for_fix_safe_enabled;
|
||||
use crate::fix::edits::pad_end;
|
||||
use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
|
||||
/// ## What it does
|
||||
@@ -85,21 +85,25 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let edit = if let Some(parenthesized_range) = parenthesized_range(
|
||||
|
||||
let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
|
||||
expr_attr.value.as_ref().into(),
|
||||
expr_attr.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source(),
|
||||
) {
|
||||
Edit::range_deletion(expr_call.range().add_start(parenthesized_range.len()))
|
||||
expr_call.range().add_start(parenthesized_range.len())
|
||||
} else {
|
||||
Edit::range_deletion(expr_call.range().add_start(expr_attr.value.range().len()))
|
||||
expr_call.range().add_start(expr_attr.value.range().len())
|
||||
};
|
||||
|
||||
let padded = pad_end(String::new(), deletion_range.end(), checker.locator());
|
||||
let edit = if padded.is_empty() {
|
||||
Edit::range_deletion(deletion_range)
|
||||
} else {
|
||||
Edit::range_replacement(padded, deletion_range)
|
||||
};
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range());
|
||||
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) {
|
||||
Fix::safe_edit(edit)
|
||||
} else {
|
||||
Fix::unsafe_edit(edit)
|
||||
});
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | # Errors
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
@@ -33,7 +33,7 @@ FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
@@ -53,7 +53,7 @@ FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
@@ -74,7 +74,7 @@ FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
@@ -93,7 +93,7 @@ FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
@@ -113,7 +113,7 @@ FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
14 14 | for _line in f.readlines():
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
@@ -133,7 +133,7 @@ FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
@@ -154,7 +154,7 @@ FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | def func():
|
||||
25 25 | f = Path("FURB129.py").open()
|
||||
@@ -173,7 +173,7 @@ FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
29 29 |
|
||||
30 30 |
|
||||
31 31 | def func(f: io.BytesIO):
|
||||
@@ -194,7 +194,7 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | def func():
|
||||
37 37 | with (open("FURB129.py") as f, foo as bar):
|
||||
@@ -213,7 +213,7 @@ FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
44 44 | import builtins
|
||||
45 45 |
|
||||
46 46 | with builtins.open("FURB129.py") as f:
|
||||
@@ -232,7 +232,7 @@ FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
51 51 | from builtins import open as o
|
||||
52 52 |
|
||||
53 53 | with o("FURB129.py") as f:
|
||||
@@ -252,7 +252,7 @@ FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
90 90 |
|
||||
91 91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 92 | with open("furb129.py") as f:
|
||||
@@ -270,7 +270,7 @@ FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
@@ -290,7 +290,7 @@ FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
@@ -310,10 +310,27 @@ FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
ℹ Safe fix
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 101 | for line in (((f))).readlines():
|
||||
102 102 | pass
|
||||
103 |- for line in(f).readlines():
|
||||
103 |+ for line in(f):
|
||||
104 104 | pass
|
||||
105 105 |
|
||||
106 106 | # Test case for issue #17683 (missing space before keyword)
|
||||
|
||||
FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
106 | # Test case for issue #17683 (missing space before keyword)
|
||||
107 | print([line for line in f.readlines()if True])
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
104 104 | pass
|
||||
105 105 |
|
||||
106 106 | # Test case for issue #17683 (missing space before keyword)
|
||||
107 |- print([line for line in f.readlines()if True])
|
||||
107 |+ print([line for line in f if True])
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
5 | # Errors
|
||||
6 | with open("FURB129.py") as f:
|
||||
7 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | # Errors
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 |- for _line in f.readlines():
|
||||
7 |+ for _line in f:
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
|
||||
FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
7 | for _line in f.readlines():
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | with open("FURB129.py") as f:
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 |- a = [line.lower() for line in f.readlines()]
|
||||
9 |+ a = [line.lower() for line in f]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
|
||||
FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
8 | pass
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | for _line in f.readlines():
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 |- b = {line.upper() for line in f.readlines()}
|
||||
10 |+ b = {line.upper() for line in f}
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
|
||||
FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
9 | a = [line.lower() for line in f.readlines()]
|
||||
10 | b = {line.upper() for line in f.readlines()}
|
||||
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
12 |
|
||||
13 | with Path("FURB129.py").open() as f:
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | pass
|
||||
9 9 | a = [line.lower() for line in f.readlines()]
|
||||
10 10 | b = {line.upper() for line in f.readlines()}
|
||||
11 |- c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
11 |+ c = {line.lower(): line.upper() for line in f}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
14 14 | for _line in f.readlines():
|
||||
|
||||
FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
13 | with Path("FURB129.py").open() as f:
|
||||
14 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
15 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
12 12 |
|
||||
13 13 | with Path("FURB129.py").open() as f:
|
||||
14 |- for _line in f.readlines():
|
||||
14 |+ for _line in f:
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
|
||||
FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
15 | pass
|
||||
16 |
|
||||
17 | for _line in open("FURB129.py").readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
18 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | for _line in f.readlines():
|
||||
15 15 | pass
|
||||
16 16 |
|
||||
17 |-for _line in open("FURB129.py").readlines():
|
||||
17 |+for _line in open("FURB129.py"):
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
20 20 | for _line in Path("FURB129.py").open().readlines():
|
||||
|
||||
FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | for _line in Path("FURB129.py").open().readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
21 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 | for _line in open("FURB129.py").readlines():
|
||||
18 18 | pass
|
||||
19 19 |
|
||||
20 |-for _line in Path("FURB129.py").open().readlines():
|
||||
20 |+for _line in Path("FURB129.py").open():
|
||||
21 21 | pass
|
||||
22 22 |
|
||||
23 23 |
|
||||
|
||||
FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
24 | def func():
|
||||
25 | f = Path("FURB129.py").open()
|
||||
26 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
27 | pass
|
||||
28 | f.close()
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | def func():
|
||||
25 25 | f = Path("FURB129.py").open()
|
||||
26 |- for _line in f.readlines():
|
||||
26 |+ for _line in f:
|
||||
27 27 | pass
|
||||
28 28 | f.close()
|
||||
29 29 |
|
||||
|
||||
FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
31 | def func(f: io.BytesIO):
|
||||
32 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
33 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
29 29 |
|
||||
30 30 |
|
||||
31 31 | def func(f: io.BytesIO):
|
||||
32 |- for _line in f.readlines():
|
||||
32 |+ for _line in f:
|
||||
33 33 | pass
|
||||
34 34 |
|
||||
35 35 |
|
||||
|
||||
FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
36 | def func():
|
||||
37 | with (open("FURB129.py") as f, foo as bar):
|
||||
38 | for _line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
39 | pass
|
||||
40 | for _line in bar.readlines():
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | def func():
|
||||
37 37 | with (open("FURB129.py") as f, foo as bar):
|
||||
38 |- for _line in f.readlines():
|
||||
38 |+ for _line in f:
|
||||
39 39 | pass
|
||||
40 40 | for _line in bar.readlines():
|
||||
41 41 | pass
|
||||
|
||||
FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
46 | with builtins.open("FURB129.py") as f:
|
||||
47 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
48 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
44 44 | import builtins
|
||||
45 45 |
|
||||
46 46 | with builtins.open("FURB129.py") as f:
|
||||
47 |- for line in f.readlines():
|
||||
47 |+ for line in f:
|
||||
48 48 | pass
|
||||
49 49 |
|
||||
50 50 |
|
||||
|
||||
FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
53 | with o("FURB129.py") as f:
|
||||
54 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
55 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
51 51 | from builtins import open as o
|
||||
52 52 |
|
||||
53 53 | with o("FURB129.py") as f:
|
||||
54 |- for line in f.readlines():
|
||||
54 |+ for line in f:
|
||||
55 55 | pass
|
||||
56 56 |
|
||||
57 57 |
|
||||
|
||||
FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 | with open("furb129.py") as f:
|
||||
93 | for line in (f).readlines():
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
94 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 |
|
||||
91 91 | # https://github.com/astral-sh/ruff/issues/18231
|
||||
92 92 | with open("furb129.py") as f:
|
||||
93 |- for line in (f).readlines():
|
||||
93 |+ for line in (f):
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
|
||||
FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
96 | with open("furb129.py") as f:
|
||||
97 | [line for line in (f).readlines()]
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
94 94 | pass
|
||||
95 95 |
|
||||
96 96 | with open("furb129.py") as f:
|
||||
97 |- [line for line in (f).readlines()]
|
||||
97 |+ [line for line in (f)]
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
|
||||
FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
100 | with open("furb129.py") as f:
|
||||
101 | for line in (((f))).readlines():
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB129
|
||||
102 | pass
|
||||
103 | for line in(f).readlines():
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
98 98 |
|
||||
99 99 |
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 |- for line in (((f))).readlines():
|
||||
101 |+ for line in (((f))):
|
||||
102 102 | pass
|
||||
103 103 | for line in(f).readlines():
|
||||
104 104 | pass
|
||||
|
||||
FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
101 | for line in (((f))).readlines():
|
||||
102 | pass
|
||||
103 | for line in(f).readlines():
|
||||
| ^^^^^^^^^^^^^^^ FURB129
|
||||
104 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Safe fix
|
||||
100 100 | with open("furb129.py") as f:
|
||||
101 101 | for line in (((f))).readlines():
|
||||
102 102 | pass
|
||||
103 |- for line in(f).readlines():
|
||||
103 |+ for line in(f):
|
||||
104 104 | pass
|
||||
@@ -24,6 +24,7 @@ mod tests {
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))]
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
|
||||
#[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))]
|
||||
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
|
||||
@@ -94,6 +95,7 @@ mod tests {
|
||||
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
|
||||
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_0.py"))]
|
||||
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))]
|
||||
@@ -322,10 +324,7 @@ mod tests {
|
||||
fn ruff_noqa_filedirective_unused() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_6.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA])
|
||||
},
|
||||
&settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]),
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
@@ -335,15 +334,12 @@ mod tests {
|
||||
fn ruff_noqa_filedirective_unused_last_of_many() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_7.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::FStringMissingPlaceholders,
|
||||
Rule::LineTooLong,
|
||||
Rule::UnusedVariable,
|
||||
])
|
||||
},
|
||||
&settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::FStringMissingPlaceholders,
|
||||
Rule::LineTooLong,
|
||||
Rule::UnusedVariable,
|
||||
]),
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
@@ -480,10 +476,8 @@ mod tests {
|
||||
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
|
||||
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
|
||||
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
|
||||
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -4,7 +4,6 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::preview::is_support_slices_in_literal_concatenation_enabled;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -96,7 +95,7 @@ enum Type {
|
||||
}
|
||||
|
||||
/// Recursively merge all the tuples and lists in the expression.
|
||||
fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(Expr, Type)> {
|
||||
fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> {
|
||||
let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::Add,
|
||||
@@ -108,22 +107,18 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
|
||||
};
|
||||
|
||||
let new_left = match left.as_ref() {
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => {
|
||||
match concatenate_expressions(left, should_support_slices) {
|
||||
Some((new_left, _)) => new_left,
|
||||
None => *left.clone(),
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(left) {
|
||||
Some((new_left, _)) => new_left,
|
||||
None => *left.clone(),
|
||||
},
|
||||
_ => *left.clone(),
|
||||
};
|
||||
|
||||
let new_right = match right.as_ref() {
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => {
|
||||
match concatenate_expressions(right, should_support_slices) {
|
||||
Some((new_right, _)) => new_right,
|
||||
None => *right.clone(),
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(right) {
|
||||
Some((new_right, _)) => new_right,
|
||||
None => *right.clone(),
|
||||
},
|
||||
_ => *right.clone(),
|
||||
};
|
||||
|
||||
@@ -151,9 +146,7 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
|
||||
make_splat_elts(splat_element, other_elements, splat_at_left)
|
||||
}
|
||||
// Subscripts are also considered safe-ish to splat if the indexer is a slice.
|
||||
Expr::Subscript(ast::ExprSubscript { slice, .. })
|
||||
if should_support_slices && matches!(&**slice, Expr::Slice(_)) =>
|
||||
{
|
||||
Expr::Subscript(ast::ExprSubscript { slice, .. }) if matches!(&**slice, Expr::Slice(_)) => {
|
||||
make_splat_elts(splat_element, other_elements, splat_at_left)
|
||||
}
|
||||
// If the splat element is itself a list/tuple, insert them in the other list/tuple.
|
||||
@@ -198,10 +191,7 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_support_slices =
|
||||
is_support_slices_in_literal_concatenation_enabled(checker.settings);
|
||||
|
||||
let Some((new_expr, type_)) = concatenate_expressions(expr, should_support_slices) else {
|
||||
let Some((new_expr, type_)) = concatenate_expressions(expr) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::function_type::is_stub;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::fastapi::rules::is_fastapi_route;
|
||||
|
||||
/// ## What it does
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -9,6 +8,7 @@ snapshot_kind: text
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
@@ -44,6 +44,7 @@ i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
|
||||
@@ -26,5 +26,5 @@ pub fn is_pragma_comment(comment: &str) -> bool {
|
||||
// Case-sensitive match against a variety of pragmas that _do_ require a trailing colon.
|
||||
trimmed
|
||||
.split_once(':')
|
||||
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff"))
|
||||
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff" | "ty"))
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ fn syntax_error() {
|
||||
fn unsupported_syntax_error() {
|
||||
check!(
|
||||
"match 2:\n case 1: ...",
|
||||
r#"{"preview": true, "target-version": "py39"}"#,
|
||||
r#"{"target-version": "py39"}"#,
|
||||
[ExpandedMessage {
|
||||
code: None,
|
||||
message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
206
crates/ty/tests/cli/config_option.rs
Normal file
206
crates/ty/tests/cli/config_option.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
// Long flag
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @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.
|
||||
");
|
||||
|
||||
// Short flag
|
||||
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files(vec![
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
])?;
|
||||
|
||||
// Exit code of 1 due to the setting in `ty.toml`
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @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.
|
||||
");
|
||||
|
||||
// Exit code of 0 because the `ty.toml` setting is overwritten by `--config`
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(1)")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: TOML parse error at line 1, column 1
|
||||
|
|
||||
1 | bad-option=true
|
||||
| ^^^^^^^^^^
|
||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`
|
||||
|
||||
|
||||
Usage: ty <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
|
||||
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 = CliTest::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(())
|
||||
}
|
||||
272
crates/ty/tests/cli/exit_code.rs
Normal file
272
crates/ty/tests/cli/exit_code.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn only_warnings() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_info() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- 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 both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r###"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- 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 exit_zero_is_true() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^
|
||||
|
|
||||
info: rule `non-subscriptable` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
690
crates/ty/tests/cli/main.rs
Normal file
690
crates/ty/tests/cli/main.rs
Normal file
@@ -0,0 +1,690 @@
|
||||
mod config_option;
|
||||
mod exit_code;
|
||||
mod python_environment;
|
||||
mod rule_selection;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_run_in_sub_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> <temp_dir>/test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([(".test.py", "~")])?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> .test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_respect_ignore_files() -> anyhow::Result<()> {
|
||||
// First test that the default option works correctly (the file is skipped)
|
||||
let case = CliTest::with_files([(".ignore", "test.py"), ("test.py", "~")])?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
WARN No python files found under the given path(s)
|
||||
");
|
||||
|
||||
// Test that we can set to false via CLI
|
||||
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
// Test that we can set to false via config file
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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 CLI takes precedence
|
||||
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[invalid-syntax]
|
||||
--> test.py:1:2
|
||||
|
|
||||
1 | ~
|
||||
| ^ Expected an expression
|
||||
|
|
||||
|
||||
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
|
||||
/// running the CLI from the child directory (using relative paths).
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
///
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `utils`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from utils import add
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
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().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- 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 in a configuration file are relative to the project root.
|
||||
///
|
||||
/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run
|
||||
/// the CLI from a subdirectory.
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
#[test]
|
||||
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
extra-paths = ["libs"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- 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 user_configuration() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/ty.toml",
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "warn"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/main.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
let config_directory = case.root().join("home/.config");
|
||||
let config_env_var = if cfg!(windows) {
|
||||
"APPDATA"
|
||||
} else {
|
||||
"XDG_CONFIG_HOME"
|
||||
};
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"
|
||||
);
|
||||
|
||||
// The user-level configuration sets the severity for `unresolved-reference` to warn.
|
||||
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
|
||||
// has higher precedence.
|
||||
case.write_file(
|
||||
config_directory.join("ty/ty.toml"),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "error"
|
||||
unresolved-reference = "warn"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
warning[unresolved-reference]: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected in the configuration file
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- 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 check_specific_paths() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/main.py",
|
||||
r#"
|
||||
y = 4 / 0 # error: division-by-zero
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/tests/test_main.py",
|
||||
r#"
|
||||
import does_not_exist # error: unresolved-import
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/other.py",
|
||||
r#"
|
||||
from main2 import z # error: unresolved-import
|
||||
|
||||
print(z)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command(),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"###
|
||||
);
|
||||
|
||||
// Now check only the `tests` and `other.py` files.
|
||||
// We should no longer see any diagnostics related to `main.py`.
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/tests").arg("project/other.py"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `main2`
|
||||
--> project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `does_not_exist`
|
||||
--> project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- 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 check_non_existing_path() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([])?;
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(
|
||||
®ex::escape("The system cannot find the path specified. (os error 3)"),
|
||||
"No such file or directory (os error 2)",
|
||||
);
|
||||
let _s = settings.bind_to_scope();
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/main.py").arg("project/tests"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[io]: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
|
||||
|
||||
error[io]: `<temp_dir>/project/tests`: No such file or directory (os error 2)
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
WARN No python files found under the given path(s)
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference] test.py:2:7: Name `x` used when not defined
|
||||
error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests the diagnostic format for revealed type.
|
||||
///
|
||||
/// This test was introduced because changes were made to
|
||||
/// how the revealed type diagnostic was constructed and
|
||||
/// formatted in "verbose" mode. But it required extra
|
||||
/// logic to ensure the concise version didn't regress on
|
||||
/// information content. So this test was introduced to
|
||||
/// capture that.
|
||||
#[test]
|
||||
fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
x = "hello"
|
||||
reveal_type(x)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
let mut content = String::new();
|
||||
writeln!(
|
||||
&mut content,
|
||||
"
|
||||
from typing_extensions import reveal_type
|
||||
total = 1{plus_one_repeated}
|
||||
reveal_type(total)
|
||||
",
|
||||
plus_one_repeated = " + 1".repeat(2000 - 1)
|
||||
)?;
|
||||
|
||||
let case = CliTest::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:4:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
|
||||
4 | reveal_type(total)
|
||||
| ^^^^^ `Literal[2000]`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub(crate) struct CliTest {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
project_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl CliTest {
|
||||
pub(crate) fn new() -> anyhow::Result<Self> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
|
||||
// and that doesn't play well with our snapshot filtering.
|
||||
// Simplify with dunce because otherwise we get UNC paths on Windows.
|
||||
let project_dir = dunce::simplified(
|
||||
&temp_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize project path")?,
|
||||
)
|
||||
.to_path_buf();
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
|
||||
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
|
||||
let settings_scope = settings.bind_to_scope();
|
||||
|
||||
Ok(Self {
|
||||
project_dir,
|
||||
_temp_dir: temp_dir,
|
||||
_settings_scope: settings_scope,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_files<'a>(
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_file(path, content)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
pub(crate) fn write_files<'a>(
|
||||
&self,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<()> {
|
||||
for (path, content) in files {
|
||||
self.write_file(path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let path = self.project_dir.join(path);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
|
||||
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn root(&self) -> &Path {
|
||||
&self.project_dir
|
||||
}
|
||||
|
||||
pub(crate) fn command(&self) -> Command {
|
||||
let mut command = Command::new(get_cargo_bin("ty"));
|
||||
command.current_dir(&self.project_dir).arg("check");
|
||||
|
||||
// Unset environment variables that can affect test behavior
|
||||
command.env_remove("VIRTUAL_ENV");
|
||||
command.env_remove("CONDA_PREFIX");
|
||||
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
fn tempdir_filter(path: &Path) -> String {
|
||||
format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
|
||||
}
|
||||
774
crates/ty/tests/cli/python_environment.rs
Normal file
774
crates/ty/tests/cli/python_environment.rs
Normal file
@@ -0,0 +1,774 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration. Here, this is tested for the Python version.
|
||||
#[test]
|
||||
fn config_override_python_version() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
# Access `sys.last_exc` that was only added in Python 3.12
|
||||
print(sys.last_exc)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-attribute]: Type `<module 'sys'>` has no attribute `last_exc`
|
||||
--> test.py:5:7
|
||||
|
|
||||
4 | # Access `sys.last_exc` that was only added in Python 3.12
|
||||
5 | print(sys.last_exc)
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
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").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as above, but for the Python platform.
|
||||
#[test]
|
||||
fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
reveal_type(sys.platform)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `Literal["linux"]`
|
||||
|
|
||||
|
||||
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-platform").arg("all"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `LiteralString`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python-version = "3.8"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
aiter
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.8 was assumed when resolving types
|
||||
--> pyproject.toml:3:18
|
||||
|
|
||||
2 | [tool.ty.environment]
|
||||
3 | python-version = "3.8"
|
||||
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:2:1
|
||||
|
|
||||
2 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.9 was assumed when resolving types because it was specified on the command line
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "venv"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"venv/pyvenv.cfg",
|
||||
r#"
|
||||
version = 3.8
|
||||
home = foo/bar/bin
|
||||
"#,
|
||||
),
|
||||
if cfg!(target_os = "windows") {
|
||||
("foo/bar/bin/python.exe", "")
|
||||
} else {
|
||||
("foo/bar/bin/python", "")
|
||||
},
|
||||
if cfg!(target_os = "windows") {
|
||||
("venv/Lib/site-packages/foo.py", "")
|
||||
} else {
|
||||
("venv/lib/python3.8/site-packages/foo.py", "")
|
||||
},
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:1:1
|
||||
|
|
||||
1 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.8 was assumed when resolving types because of your virtual environment
|
||||
--> venv/pyvenv.cfg:2:11
|
||||
|
|
||||
2 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
3 | home = foo/bar/bin
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "venv"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"venv/pyvenv.cfg",
|
||||
r#"home = foo/bar/bin
|
||||
|
||||
|
||||
version = 3.8"#,
|
||||
),
|
||||
if cfg!(target_os = "windows") {
|
||||
("foo/bar/bin/python.exe", "")
|
||||
} else {
|
||||
("foo/bar/bin/python", "")
|
||||
},
|
||||
if cfg!(target_os = "windows") {
|
||||
("venv/Lib/site-packages/foo.py", "")
|
||||
} else {
|
||||
("venv/lib/python3.8/site-packages/foo.py", "")
|
||||
},
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `aiter` used when not defined
|
||||
--> test.py:1:1
|
||||
|
|
||||
1 | aiter
|
||||
| ^^^^^
|
||||
|
|
||||
info: `aiter` was added as a builtin in Python 3.10
|
||||
info: Python 3.8 was assumed when resolving types because of your virtual environment
|
||||
--> venv/pyvenv.cfg:4:23
|
||||
|
|
||||
4 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
|
||||
let case = CliTest::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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
|
||||
let path_to_executable = if cfg!(windows) {
|
||||
"my-venv/Scripts/python.exe"
|
||||
} else {
|
||||
"my-venv/bin/python"
|
||||
};
|
||||
|
||||
let other_venv_path = "my-venv/foo/some_other_file.txt";
|
||||
|
||||
let case = CliTest::with_files([
|
||||
("test.py", ""),
|
||||
(
|
||||
if cfg!(windows) {
|
||||
"my-venv/Lib/site-packages/foo.py"
|
||||
} else {
|
||||
"my-venv/lib/python3.13/site-packages/foo.py"
|
||||
},
|
||||
"",
|
||||
),
|
||||
(path_to_executable, ""),
|
||||
(other_venv_path, ""),
|
||||
])?;
|
||||
|
||||
// Passing a path to the installation works
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// And so does passing a path to the executable inside the installation
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// But random other paths inside the installation are rejected
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk
|
||||
");
|
||||
|
||||
// And so are paths that do not exist on disk
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_cli_argument_system_installation() -> anyhow::Result<()> {
|
||||
let path_to_executable = if cfg!(windows) {
|
||||
"Python3.11/python.exe"
|
||||
} else {
|
||||
"Python3.11/bin/python"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
("test.py", ""),
|
||||
(
|
||||
if cfg!(windows) {
|
||||
"Python3.11/Lib/site-packages/foo.py"
|
||||
} else {
|
||||
"Python3.11/lib/python3.11/site-packages/foo.py"
|
||||
},
|
||||
"",
|
||||
),
|
||||
(path_to_executable, ""),
|
||||
])?;
|
||||
|
||||
// Passing a path to the installation works
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// And so does passing a path to the executable inside the installation
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- 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_broken_python_setting() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
description = "Some description"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
|
||||
[tool.ty.environment]
|
||||
python = "not-a-directory-or-executable"
|
||||
"#,
|
||||
),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
9 |
|
||||
10 | [tool.ty.environment]
|
||||
11 | python = "not-a-directory-or-executable"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "directory-but-no-site-packages"
|
||||
"#,
|
||||
),
|
||||
("directory-but-no-site-packages/lib/foo.py", ""),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
1 |
|
||||
2 | [tool.ty.environment]
|
||||
3 | python = "directory-but-no-site-packages"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This error message is never emitted on Windows, because Windows installations have simpler layouts
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.environment]
|
||||
python = "directory-but-no-site-packages"
|
||||
"#,
|
||||
),
|
||||
("directory-but-no-site-packages/foo.py", ""),
|
||||
("test.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Invalid search path settings
|
||||
Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
1 |
|
||||
2 | [tool.ty.environment]
|
||||
3 | python = "directory-but-no-site-packages"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"ty.toml",
|
||||
&*format!(
|
||||
r#"
|
||||
[environment]
|
||||
python-version = "{}"
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
PythonVersion::default()
|
||||
),
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt`
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | import os
|
||||
3 |
|
||||
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
// Use default (which should be latest supported)
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[environment]
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `site-packages` directory is used by ty for external import.
|
||||
/// Ty does the following checks to discover the `site-packages` directory in the order:
|
||||
/// 1) If `VIRTUAL_ENV` environment variable is set
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set
|
||||
/// 3) If a `.venv` directory exists at the project root
|
||||
///
|
||||
/// This test is aiming at validating the logic around `CONDA_PREFIX`.
|
||||
///
|
||||
/// A conda-like environment file structure is used
|
||||
/// We test by first not setting the `CONDA_PREFIX` and expect a fail.
|
||||
/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass.
|
||||
///
|
||||
/// ├── project
|
||||
/// │ └── test.py
|
||||
/// └── conda-env
|
||||
/// └── lib
|
||||
/// └── python3.13
|
||||
/// └── site-packages
|
||||
/// └── package1
|
||||
/// └── __init__.py
|
||||
///
|
||||
/// test.py imports package1
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
||||
let conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
import package1
|
||||
"#,
|
||||
),
|
||||
(
|
||||
conda_package1_path,
|
||||
r#"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import package1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
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.
|
||||
");
|
||||
|
||||
// do command : CONDA_PREFIX=<temp_dir>/conda_env
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
292
crates/ty/tests/cli/rule_selection.rs
Normal file
292
crates/ty/tests/cli/rule_selection.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
/// The rule severity can be changed in the configuration file
|
||||
#[test]
|
||||
fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error).
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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.
|
||||
"###);
|
||||
|
||||
case.write_file(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.rules]
|
||||
division-by-zero = "warn" # promote to warn
|
||||
unresolved-reference = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error`
|
||||
#[test]
|
||||
fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
import does_not_exit
|
||||
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error)
|
||||
// and an unresolved-import (error) diagnostic by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:9:1
|
||||
|
|
||||
7 | x = a
|
||||
8 |
|
||||
9 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- 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("--ignore")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-import"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unresolved-import]: Cannot resolve imported module `does_not_exit`
|
||||
--> test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
|
||||
info: rule `unresolved-import` was selected on the command line
|
||||
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> test.py:4:5
|
||||
|
|
||||
2 | import does_not_exit
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
| ^^^^^
|
||||
5 |
|
||||
6 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` was selected on the command line
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and
|
||||
/// values specified last override previous severities.
|
||||
#[test]
|
||||
fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a `unresolved-reference` diagnostic (error) by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
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("--warn")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--ignore")
|
||||
.arg("unresolved-reference"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
3 |
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
info: rule `division-by-zero` 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(())
|
||||
}
|
||||
|
||||
/// ty warns about unknown rules specified in a configuration file
|
||||
#[test]
|
||||
fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.rules]
|
||||
division-by-zer = "warn" # incorrect rule name
|
||||
"#,
|
||||
),
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unknown-rule]
|
||||
--> pyproject.toml:3:1
|
||||
|
|
||||
2 | [tool.ty.rules]
|
||||
3 | division-by-zer = "warn" # incorrect rule name
|
||||
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
|
||||
|
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// ty warns about unknown rules specified in a CLI argument
|
||||
#[test]
|
||||
fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file("test.py", "print(10)")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[unknown-rule]: Unknown lint rule `division-by-zer`
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
from typing import Literal
|
||||
|
||||
class Format:
|
||||
STRING = "string"
|
||||
|
||||
def evaluate(a: Literal[Format.STRING], b: Literal[-1]) -> str: ...
|
||||
@@ -192,12 +192,12 @@ impl Options {
|
||||
PythonPath::sys_prefix(python_path.absolute(project_root, system), origin)
|
||||
})
|
||||
.or_else(|| {
|
||||
std::env::var("VIRTUAL_ENV").ok().map(|virtual_env| {
|
||||
system.env_var("VIRTUAL_ENV").ok().map(|virtual_env| {
|
||||
PythonPath::sys_prefix(virtual_env, SysPrefixPathOrigin::VirtualEnvVar)
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
std::env::var("CONDA_PREFIX").ok().map(|path| {
|
||||
system.env_var("CONDA_PREFIX").ok().map(|path| {
|
||||
PythonPath::sys_prefix(path, SysPrefixPathOrigin::CondaPrefixVar)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,7 +59,6 @@ fn linter_gz_no_panic() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Enable running once there are fewer failures"]
|
||||
fn linter_stubs_no_panic() -> anyhow::Result<()> {
|
||||
let workspace_root = get_cargo_workspace_root()?;
|
||||
run_corpus_tests(&format!(
|
||||
@@ -68,7 +67,6 @@ fn linter_stubs_no_panic() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Enable running over typeshed stubs once there are fewer failures"]
|
||||
fn typeshed_no_panic() -> anyhow::Result<()> {
|
||||
let workspace_root = get_cargo_workspace_root()?;
|
||||
run_corpus_tests(&format!(
|
||||
@@ -119,6 +117,11 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
|
||||
let code = std::fs::read_to_string(source)?;
|
||||
|
||||
let mut check_with_file_name = |path: &SystemPath| {
|
||||
if DO_NOT_ATTEMPT.contains(&&*relative_path.as_str().replace('\\', "/")) {
|
||||
println!("Skipping {relative_path:?} due to known stack overflow");
|
||||
return;
|
||||
}
|
||||
|
||||
memory_fs.write_file_all(path, &code).unwrap();
|
||||
File::sync_path(&mut db, path);
|
||||
|
||||
@@ -298,4 +301,16 @@ const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||
// Fails with too-many-cycle-iterations due to a self-referential
|
||||
// type alias, see https://github.com/astral-sh/ty/issues/256
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_34.py", true, true),
|
||||
|
||||
// These are all "expression should belong to this TypeInference region and TypeInferenceBuilder should have inferred a type for it"
|
||||
("crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi", true, true),
|
||||
("crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi", true, true),
|
||||
("crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi", true, true),
|
||||
];
|
||||
|
||||
/// Attempting to check one of these files causes a stack overflow
|
||||
const DO_NOT_ATTEMPT: &[&str] = &[
|
||||
"crates/ty_vendored/vendor/typeshed/stdlib/pathlib/types.pyi",
|
||||
"crates/ty_vendored/vendor/typeshed/stdlib/types.pyi",
|
||||
"crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi",
|
||||
];
|
||||
|
||||
@@ -47,7 +47,7 @@ def _(flag: bool):
|
||||
def _(x: Annotated | bool):
|
||||
reveal_type(x) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)"
|
||||
def _(x: Annotated[()]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
|
||||
@@ -116,6 +116,21 @@ def _(c: Callable[
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### Tuple as the second argument
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# fmt: off
|
||||
|
||||
def _(c: Callable[
|
||||
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
(str, ) # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### List as both arguments
|
||||
|
||||
```py
|
||||
|
||||
@@ -95,6 +95,11 @@ async def outer(): # avoid unrelated syntax errors on yield, yield from, and aw
|
||||
|
||||
## Invalid Collection based AST nodes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
def _(
|
||||
a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions"
|
||||
@@ -103,7 +108,11 @@ def _(
|
||||
d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions"
|
||||
e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions"
|
||||
f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions"
|
||||
g: [int, str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?"
|
||||
g: [int, str],
|
||||
# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?"
|
||||
h: (int, str),
|
||||
i: (), # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -112,6 +121,17 @@ def _(
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: Unknown
|
||||
reveal_type(i) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?"
|
||||
class name_0[name_2: [int]]:
|
||||
pass
|
||||
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
# error: [invalid-type-form] "Dict literals are not allowed in type expressions"
|
||||
class name_4[name_1: [{}]]:
|
||||
pass
|
||||
```
|
||||
|
||||
## Diagnostics for common errors
|
||||
@@ -145,3 +165,42 @@ from PIL import Image
|
||||
|
||||
def g(x: Image): ... # error: [invalid-type-form]
|
||||
```
|
||||
|
||||
### List-literal used when you meant to use a list or tuple
|
||||
|
||||
```py
|
||||
def _(
|
||||
x: [int], # error: [invalid-type-form]
|
||||
) -> [int]: # error: [invalid-type-form]
|
||||
return x
|
||||
```
|
||||
|
||||
```py
|
||||
def _(
|
||||
x: [int, str], # error: [invalid-type-form]
|
||||
) -> [int, str]: # error: [invalid-type-form]
|
||||
return x
|
||||
```
|
||||
|
||||
### Tuple-literal used when you meant to use a tuple
|
||||
|
||||
```py
|
||||
def _(
|
||||
x: (), # error: [invalid-type-form]
|
||||
) -> (): # error: [invalid-type-form]
|
||||
return x
|
||||
```
|
||||
|
||||
```py
|
||||
def _(
|
||||
x: (int,), # error: [invalid-type-form]
|
||||
) -> (int,): # error: [invalid-type-form]
|
||||
return x
|
||||
```
|
||||
|
||||
```py
|
||||
def _(
|
||||
x: (int, str), # error: [invalid-type-form]
|
||||
) -> (int, str): # error: [invalid-type-form]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -305,10 +305,13 @@ simplify to `Never`, even in the presence of other types:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(Generic[T_co]): ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Not[P]],
|
||||
@@ -317,6 +320,8 @@ def _(
|
||||
i4: Intersection[Not[P], Q, P],
|
||||
i5: Intersection[P, Any, Not[P]],
|
||||
i6: Intersection[Not[P], Any, P],
|
||||
i7: Intersection[R[P], Not[R[P]]],
|
||||
i8: Intersection[R[P], Not[R[Q]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
@@ -324,6 +329,8 @@ def _(
|
||||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
reveal_type(i7) # revealed: Never
|
||||
reveal_type(i8) # revealed: R[P] & ~R[Q]
|
||||
```
|
||||
|
||||
### Union of a type and its negation
|
||||
@@ -332,20 +339,28 @@ Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `o
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(Generic[T_co]): ...
|
||||
|
||||
def _(
|
||||
i1: P | Not[P],
|
||||
i2: Not[P] | P,
|
||||
i3: P | Q | Not[P],
|
||||
i4: Not[P] | Q | P,
|
||||
i5: R[P] | Not[R[P]],
|
||||
i6: R[P] | Not[R[Q]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: object
|
||||
reveal_type(i2) # revealed: object
|
||||
reveal_type(i3) # revealed: object
|
||||
reveal_type(i4) # revealed: object
|
||||
reveal_type(i5) # revealed: object
|
||||
reveal_type(i6) # revealed: R[P] | ~R[Q]
|
||||
```
|
||||
|
||||
### Negation is an involution
|
||||
|
||||
@@ -902,8 +902,7 @@ from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf
|
||||
class HasX(Protocol):
|
||||
x: int
|
||||
|
||||
# TODO: this should pass
|
||||
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[module], HasX))
|
||||
static_assert(is_assignable_to(TypeOf[module], HasX))
|
||||
|
||||
class ExplicitProtocolSubtype(HasX, Protocol):
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - List-literal used when you meant to use a list or tuple
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def _(
|
||||
2 | x: [int], # error: [invalid-type-form]
|
||||
3 | ) -> [int]: # error: [invalid-type-form]
|
||||
4 | return x
|
||||
5 | def _(
|
||||
6 | x: [int, str], # error: [invalid-type-form]
|
||||
7 | ) -> [int, str]: # error: [invalid-type-form]
|
||||
8 | return x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:2:8
|
||||
|
|
||||
1 | def _(
|
||||
2 | x: [int], # error: [invalid-type-form]
|
||||
| ^^^^^ Did you mean `list[int]`?
|
||||
3 | ) -> [int]: # error: [invalid-type-form]
|
||||
4 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:3:6
|
||||
|
|
||||
1 | def _(
|
||||
2 | x: [int], # error: [invalid-type-form]
|
||||
3 | ) -> [int]: # error: [invalid-type-form]
|
||||
| ^^^^^ Did you mean `list[int]`?
|
||||
4 | return x
|
||||
5 | def _(
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:6:8
|
||||
|
|
||||
4 | return x
|
||||
5 | def _(
|
||||
6 | x: [int, str], # error: [invalid-type-form]
|
||||
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
|
||||
7 | ) -> [int, str]: # error: [invalid-type-form]
|
||||
8 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:7:6
|
||||
|
|
||||
5 | def _(
|
||||
6 | x: [int, str], # error: [invalid-type-form]
|
||||
7 | ) -> [int, str]: # error: [invalid-type-form]
|
||||
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
|
||||
8 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Tuple-literal used when you meant to use a tuple
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def _(
|
||||
2 | x: (), # error: [invalid-type-form]
|
||||
3 | ) -> (): # error: [invalid-type-form]
|
||||
4 | return x
|
||||
5 | def _(
|
||||
6 | x: (int,), # error: [invalid-type-form]
|
||||
7 | ) -> (int,): # error: [invalid-type-form]
|
||||
8 | return x
|
||||
9 | def _(
|
||||
10 | x: (int, str), # error: [invalid-type-form]
|
||||
11 | ) -> (int, str): # error: [invalid-type-form]
|
||||
12 | return x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:2:8
|
||||
|
|
||||
1 | def _(
|
||||
2 | x: (), # error: [invalid-type-form]
|
||||
| ^^ Did you mean `tuple[()]`?
|
||||
3 | ) -> (): # error: [invalid-type-form]
|
||||
4 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:3:6
|
||||
|
|
||||
1 | def _(
|
||||
2 | x: (), # error: [invalid-type-form]
|
||||
3 | ) -> (): # error: [invalid-type-form]
|
||||
| ^^ Did you mean `tuple[()]`?
|
||||
4 | return x
|
||||
5 | def _(
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:6:8
|
||||
|
|
||||
4 | return x
|
||||
5 | def _(
|
||||
6 | x: (int,), # error: [invalid-type-form]
|
||||
| ^^^^^^ Did you mean `tuple[int]`?
|
||||
7 | ) -> (int,): # error: [invalid-type-form]
|
||||
8 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:7:6
|
||||
|
|
||||
5 | def _(
|
||||
6 | x: (int,), # error: [invalid-type-form]
|
||||
7 | ) -> (int,): # error: [invalid-type-form]
|
||||
| ^^^^^^ Did you mean `tuple[int]`?
|
||||
8 | return x
|
||||
9 | def _(
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:10:8
|
||||
|
|
||||
8 | return x
|
||||
9 | def _(
|
||||
10 | x: (int, str), # error: [invalid-type-form]
|
||||
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
|
||||
11 | ) -> (int, str): # error: [invalid-type-form]
|
||||
12 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression
|
||||
--> src/mdtest_snippet.py:11:6
|
||||
|
|
||||
9 | def _(
|
||||
10 | x: (int, str), # error: [invalid-type-form]
|
||||
11 | ) -> (int, str): # error: [invalid-type-form]
|
||||
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
|
||||
12 | return x
|
||||
|
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
@@ -209,6 +209,34 @@ class AnyMeta(metaclass=Any): ...
|
||||
static_assert(is_assignable_to(type[AnyMeta], type))
|
||||
static_assert(is_assignable_to(type[AnyMeta], type[object]))
|
||||
static_assert(is_assignable_to(type[AnyMeta], type[Any]))
|
||||
|
||||
from typing import TypeVar, Generic, Any
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class Foo(Generic[T_co]): ...
|
||||
class Bar(Foo[T_co], Generic[T_co]): ...
|
||||
|
||||
static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
|
||||
|
||||
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types)
|
||||
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## `type[]` is not assignable to types disjoint from `builtins.type`
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import is_assignable_to, static_assert
|
||||
|
||||
static_assert(not is_assignable_to(type[Any], None))
|
||||
```
|
||||
|
||||
## Class-literals that inherit from `Any`
|
||||
@@ -717,6 +745,53 @@ def f(x: int, y: str) -> None: ...
|
||||
c1: Callable[[int], None] = partial(f, y="a")
|
||||
```
|
||||
|
||||
### Generic classes with `__call__`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
class Foo[T]:
|
||||
def __call__(self): ...
|
||||
|
||||
class FooLegacy(Generic[T]):
|
||||
def __call__(self): ...
|
||||
|
||||
class Bar[T, **P]:
|
||||
def __call__(self): ...
|
||||
|
||||
# TODO: should not error
|
||||
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
def __call__(self): ...
|
||||
|
||||
static_assert(is_assignable_to(Foo, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(FooLegacy, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(Bar, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
|
||||
|
||||
class Spam[T]: ...
|
||||
class SpamLegacy(Generic[T]): ...
|
||||
class Eggs[T, **P]: ...
|
||||
|
||||
# TODO: should not error
|
||||
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
|
||||
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
|
||||
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
|
||||
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))
|
||||
|
||||
# TODO: should pass
|
||||
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
### Classes with `__call__` as attribute
|
||||
|
||||
An instance type is assignable to a compatible callable type if the instance type's class has a
|
||||
|
||||
@@ -611,6 +611,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::GenericAlias(_))
|
||||
}
|
||||
|
||||
const fn is_dynamic(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
/// Replace references to the class `class` with a self-reference marker. This is currently
|
||||
/// used for recursive protocols, but could probably be extended to self-referential type-
|
||||
/// aliases and similar.
|
||||
@@ -1050,34 +1054,26 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
// Two equivalent types are always subtypes of each other.
|
||||
//
|
||||
// "Equivalent to" here means that the two types are both fully static
|
||||
// and describe exactly the same set of possible runtime objects.
|
||||
// For example, `int` is a subtype of `int` because `int` and `int` are equivalent to each other.
|
||||
// Equally, `type[object]` is a subtype of `type`,
|
||||
// because the former type expresses "all subclasses of `object`"
|
||||
// while the latter expresses "all instances of `type`",
|
||||
// and these are exactly the same set of objects at runtime.
|
||||
if self.is_equivalent_to(db, target) {
|
||||
self.has_relation_to(db, target, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.has_relation_to(db, target, TypeRelation::Assignability)
|
||||
}
|
||||
|
||||
fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool {
|
||||
if !relation.applies_to(db, self, target) {
|
||||
return false;
|
||||
}
|
||||
if relation.are_equivalent(db, self, target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-fully-static types do not participate in subtyping.
|
||||
//
|
||||
// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
|
||||
// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
|
||||
// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
|
||||
// so the question is simply unanswerable for non-fully-static types.
|
||||
if !self.is_fully_static(db) || !target.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (self, target) {
|
||||
// We should have handled these immediately above.
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => {
|
||||
unreachable!("Non-fully-static types do not participate in subtyping!")
|
||||
}
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true,
|
||||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// It is a subtype of all other fully static types.
|
||||
@@ -1115,12 +1111,12 @@ impl<'db> Type<'db> {
|
||||
match typevar.bound_or_constraints(db) {
|
||||
None => unreachable!(),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.is_subtype_of(db, target)
|
||||
bound.has_relation_to(db, target, relation)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| constraint.is_subtype_of(db, target)),
|
||||
.all(|constraint| constraint.has_relation_to(db, target, relation)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,7 +1127,7 @@ impl<'db> Type<'db> {
|
||||
if typevar.constraints(db).is_some_and(|constraints| {
|
||||
constraints
|
||||
.iter()
|
||||
.all(|constraint| self.is_subtype_of(db, *constraint))
|
||||
.all(|constraint| self.has_relation_to(db, *constraint, relation))
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
@@ -1140,12 +1136,12 @@ impl<'db> Type<'db> {
|
||||
(Type::Union(union), _) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
.all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
|
||||
|
||||
(_, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
|
||||
.any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)),
|
||||
|
||||
// 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,
|
||||
@@ -1154,7 +1150,7 @@ impl<'db> Type<'db> {
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.all(|&pos_ty| self.is_subtype_of(db, pos_ty))
|
||||
.all(|&pos_ty| self.has_relation_to(db, pos_ty, relation))
|
||||
&& intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
@@ -1164,7 +1160,7 @@ impl<'db> Type<'db> {
|
||||
(Type::Intersection(intersection), _) => intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
.any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
|
||||
|
||||
// Other than the special cases checked above, no other types are a subtype of a
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized to.
|
||||
@@ -1179,7 +1175,7 @@ impl<'db> Type<'db> {
|
||||
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
|
||||
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
|
||||
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
|
||||
target.is_equivalent_to(db, Type::object(db))
|
||||
relation.are_equivalent(db, target, Type::object(db))
|
||||
}
|
||||
|
||||
// These clauses handle type variants that include function literals. A function
|
||||
@@ -1188,13 +1184,13 @@ impl<'db> Type<'db> {
|
||||
// applied to the signature. Different specializations of the same function literal are
|
||||
// only subtypes of each other if they result in the same signature.
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_subtype_of(db, target_function)
|
||||
self_function.has_relation_to(db, target_function, relation)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_subtype_of(db, target_method)
|
||||
self_method.has_relation_to(db, target_method, relation)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_subtype_of(db, target_method)
|
||||
self_method.has_relation_to(db, target_method, relation)
|
||||
}
|
||||
|
||||
// No literal type is a subtype of any other literal type, unless they are the same
|
||||
@@ -1216,6 +1212,31 @@ impl<'db> Type<'db> {
|
||||
| Type::ModuleLiteral(_),
|
||||
) => false,
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// If the type of __call__ is a subtype of a callable type, this instance is.
|
||||
// Don't add other special cases here; our subtyping of a callable type
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.has_relation_to(db, target, relation)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.has_relation_to(db, right, relation)
|
||||
}
|
||||
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
(Type::StringLiteral(_), Type::LiteralString) => true,
|
||||
|
||||
@@ -1231,45 +1252,37 @@ impl<'db> Type<'db> {
|
||||
| Type::ModuleLiteral(_),
|
||||
_,
|
||||
) => (self.literal_fallback_instance(db))
|
||||
.is_some_and(|instance| instance.is_subtype_of(db, target)),
|
||||
|
||||
// Function-like callables are subtypes of `FunctionType`
|
||||
(Type::Callable(callable), Type::NominalInstance(target))
|
||||
if callable.is_function_like(db)
|
||||
&& target.class.is_known(db, KnownClass::FunctionType) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
.is_some_and(|instance| instance.has_relation_to(db, target, relation)),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
.is_subtype_of(db, target)
|
||||
.has_relation_to(db, target, relation)
|
||||
}
|
||||
|
||||
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
|
||||
.into_callable_type(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
|
||||
// so it also, for now, just delegates to its instance fallback.
|
||||
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// The same reasoning applies for these special callable types:
|
||||
(Type::BoundMethod(_), _) => KnownClass::MethodType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
|
||||
self_callable.is_subtype_of(db, other_callable)
|
||||
self_callable.has_relation_to(db, other_callable, relation)
|
||||
}
|
||||
|
||||
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
|
||||
@@ -1277,29 +1290,15 @@ impl<'db> Type<'db> {
|
||||
false
|
||||
}
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// If the type of __call__ is a subtype of a callable type, this instance is.
|
||||
// Don't add other special cases here; our subtyping of a callable type
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.is_subtype_of(db, target)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// Function-like callables are subtypes of `FunctionType`
|
||||
(Type::Callable(callable), _)
|
||||
if callable.is_function_like(db)
|
||||
&& KnownClass::FunctionType
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.is_subtype_of(db, right)
|
||||
}
|
||||
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
(Type::Callable(_), _) => {
|
||||
// TODO: Implement subtyping between callable types and other types like
|
||||
@@ -1319,54 +1318,81 @@ impl<'db> Type<'db> {
|
||||
self_elements.len() == target_elements.len()
|
||||
&& self_elements.iter().zip(target_elements).all(
|
||||
|(self_element, target_element)| {
|
||||
self_element.is_subtype_of(db, *target_element)
|
||||
self_element.has_relation_to(db, *target_element, relation)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
|
||||
(Type::Tuple(tuple), _) => tuple.homogeneous_supertype(db).is_subtype_of(db, target),
|
||||
(Type::Tuple(tuple), _) => tuple
|
||||
.homogeneous_supertype(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
|
||||
(Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target),
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
|
||||
(Type::BoundSuper(_), _) => KnownClass::Super
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
|
||||
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
|
||||
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_some_and(|target_class| class.is_subclass_of(db, None, target_class)),
|
||||
.is_none_or(|subclass_of_class| {
|
||||
ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_some_and(|target_class| {
|
||||
ClassType::from(alias).is_subclass_of(db, target_class)
|
||||
.is_none_or(|subclass_of_class| {
|
||||
ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
|
||||
// This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`?
|
||||
(Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => {
|
||||
self_subclass_ty.is_subtype_of(db, target_subclass_ty)
|
||||
self_subclass_ty.has_relation_to(db, target_subclass_ty, relation)
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
|
||||
ClassType::NonGeneric(class_literal)
|
||||
.into_callable(db)
|
||||
.is_subtype_of(db, target)
|
||||
.has_relation_to(db, target, relation)
|
||||
}
|
||||
|
||||
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
|
||||
.into_callable(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
|
||||
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
|
||||
// is an instance of its metaclass `abc.ABCMeta`.
|
||||
(Type::ClassLiteral(class), _) => {
|
||||
class.metaclass_instance_type(db).is_subtype_of(db, target)
|
||||
}
|
||||
(Type::ClassLiteral(class), _) => class
|
||||
.metaclass_instance_type(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::GenericAlias(alias), _) => ClassType::from(alias)
|
||||
.metaclass_instance_type(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// This branch upholds two properties:
|
||||
// - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`.
|
||||
// - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`.
|
||||
//
|
||||
// This is really the same as the very first branch in this `match` statement that handles dynamic types.
|
||||
// That branch upholds two properties:
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any`
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`.
|
||||
//
|
||||
// The only difference between this branch and the first branch is that the first branch deals with the type
|
||||
// `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`.
|
||||
//
|
||||
// See also: <https://github.com/astral-sh/ty/issues/222>
|
||||
(Type::SubclassOf(subclass_of_ty), other)
|
||||
| (other, Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
|
||||
// of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str`
|
||||
@@ -1379,30 +1405,31 @@ impl<'db> Type<'db> {
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.map(|class| class.metaclass_instance_type(db))
|
||||
.is_some_and(|metaclass_instance_type| {
|
||||
metaclass_instance_type.is_subtype_of(db, target)
|
||||
}),
|
||||
.unwrap_or_else(|| KnownClass::Type.to_instance(db))
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// For example: `Type::SpecialForm(SpecialFormType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`,
|
||||
// because `Type::SpecialForm(SpecialFormType::Type)` is a set with exactly one runtime value in it
|
||||
// (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime.
|
||||
(Type::SpecialForm(left), right) => left.instance_fallback(db).is_subtype_of(db, right),
|
||||
(Type::SpecialForm(left), right) => left
|
||||
.instance_fallback(db)
|
||||
.has_relation_to(db, right, relation),
|
||||
|
||||
(Type::KnownInstance(left), right) => {
|
||||
left.instance_fallback(db).is_subtype_of(db, right)
|
||||
}
|
||||
(Type::KnownInstance(left), right) => left
|
||||
.instance_fallback(db)
|
||||
.has_relation_to(db, right, relation),
|
||||
|
||||
// `bool` is a subtype of `int`, because `bool` subclasses `int`,
|
||||
// which means that all instances of `bool` are also instances of `int`
|
||||
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
|
||||
self_instance.is_subtype_of(db, target_instance)
|
||||
self_instance.has_relation_to(db, target_instance, relation)
|
||||
}
|
||||
|
||||
(Type::PropertyInstance(_), _) => KnownClass::Property
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(_, Type::PropertyInstance(_)) => {
|
||||
self.is_subtype_of(db, KnownClass::Property.to_instance(db))
|
||||
self.has_relation_to(db, KnownClass::Property.to_instance(db), relation)
|
||||
}
|
||||
|
||||
// Other than the special cases enumerated above, `Instance` types and typevars are
|
||||
@@ -1411,292 +1438,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_gradual_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match (self, target) {
|
||||
// Never can be assigned to any type.
|
||||
(Type::Never, _) => true,
|
||||
|
||||
// The dynamic type is assignable-to and assignable-from any type.
|
||||
(Type::Dynamic(_), _) => true,
|
||||
(_, Type::Dynamic(_)) => true,
|
||||
|
||||
// All types are assignable to `object`.
|
||||
// TODO this special case might be removable once the below cases are comprehensive
|
||||
(_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
|
||||
|
||||
// In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are assignable to `S`.
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be assignable to any union containing `T`.
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true,
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
|
||||
// A typevar is assignable to its upper bound, and to something similar to the union of
|
||||
// its constraints. An unbound, unconstrained typevar has an implicit upper bound of
|
||||
// `object` (which is handled above).
|
||||
(Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => {
|
||||
match typevar.bound_or_constraints(db) {
|
||||
None => unreachable!(),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.is_assignable_to(db, target)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| constraint.is_assignable_to(db, target)),
|
||||
}
|
||||
}
|
||||
|
||||
// If the typevar is constrained, there must be multiple constraints, and the typevar
|
||||
// might be specialized to any one of them. However, the constraints do not have to be
|
||||
// disjoint, which means an lhs type might be assignable to all of the constraints.
|
||||
(_, Type::TypeVar(typevar))
|
||||
if typevar.constraints(db).is_some_and(|constraints| {
|
||||
constraints
|
||||
.iter()
|
||||
.all(|constraint| self.is_assignable_to(db, *constraint))
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// A union is assignable to a type T iff every element of the union is assignable to T.
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// A type T is assignable to a union iff T is assignable to any element of the union.
|
||||
(ty, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is assignable to (A & B) because the left is assignable to both A and B,
|
||||
// but none of A, B, or C is assignable to (A & B).
|
||||
//
|
||||
// A type S is assignable to an intersection type T if
|
||||
// S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and
|
||||
// S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]).
|
||||
(ty, Type::Intersection(intersection)) => {
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| ty.is_assignable_to(db, elem_ty))
|
||||
&& intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
.all(|&neg_ty| ty.is_disjoint_from(db, neg_ty))
|
||||
}
|
||||
|
||||
// An intersection type S is assignable to a type T if
|
||||
// Any element of S is assignable to T (e.g. `A & B` is assignable to `A`)
|
||||
// Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T.
|
||||
(Type::Intersection(intersection), ty) => intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// Other than the special cases checked above, no other types are assignable to a
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized to.
|
||||
// (If the typevar is bounded, it might be specialized to a smaller type than the
|
||||
// bound. This is true even if the bound is a final class, since the typevar can still
|
||||
// be specialized to `Never`.)
|
||||
(_, Type::TypeVar(_)) => false,
|
||||
|
||||
// A tuple type S is assignable to a tuple type T if their lengths are the same, and
|
||||
// each element of S is assignable to the corresponding element of T.
|
||||
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
|
||||
let self_elements = self_tuple.elements(db);
|
||||
let target_elements = target_tuple.elements(db);
|
||||
self_elements.len() == target_elements.len()
|
||||
&& self_elements.iter().zip(target_elements).all(
|
||||
|(self_element, target_element)| {
|
||||
self_element.is_assignable_to(db, *target_element)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// This special case is required because the left-hand side tuple might be a
|
||||
// gradual type, so we can not rely on subtyping. This allows us to assign e.g.
|
||||
// `tuple[Any, int]` to `tuple`.
|
||||
//
|
||||
// `tuple[A, B, C]` is assignable to `tuple[A | B | C, ...]`
|
||||
(Type::Tuple(tuple), _)
|
||||
if tuple.homogeneous_supertype(db).is_assignable_to(db, target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// These clauses handle type variants that include function literals. A function
|
||||
// literal is assignable to itself, and not to any other function literal. However, our
|
||||
// representation of a function literal includes any specialization that should be
|
||||
// applied to the signature. Different specializations of the same function literal are
|
||||
// only assignable to each other if they result in the same signature.
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_assignable_to(db, target_function)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_assignable_to(db, target_method)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_assignable_to(db, target_method)
|
||||
}
|
||||
|
||||
// `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can
|
||||
// materialize to any `type[...]` type.
|
||||
(Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_))
|
||||
if subclass_of_ty.is_dynamic() =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class), Type::SubclassOf(_))
|
||||
if class
|
||||
.iter_mro(db, None)
|
||||
.any(class_base::ClassBase::is_dynamic) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// Every `type[...]` is assignable to `type`
|
||||
(Type::SubclassOf(_), _)
|
||||
if KnownClass::Type
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can
|
||||
// materialize to any `type[...]` type.
|
||||
//
|
||||
// Every class literal type is also assignable to `type[Any]`, because the class
|
||||
// literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable
|
||||
// to `type[Any]`.
|
||||
(
|
||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_),
|
||||
Type::SubclassOf(target_subclass_of),
|
||||
) if target_subclass_of.is_dynamic() => true,
|
||||
|
||||
// `type[Any]` is assignable to any type that `type[object]` is assignable to, because
|
||||
// `type[Any]` can materialize to `type[object]`.
|
||||
//
|
||||
// `type[Any]` is also assignable to any subtype of `type[object]`, because all
|
||||
// subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can
|
||||
// materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to
|
||||
// `Never`.)
|
||||
(Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& (KnownClass::Type
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target)
|
||||
|| target.is_subtype_of(db, KnownClass::Type.to_instance(db))) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// Any type that is assignable to `type[object]` is also assignable to `type[Any]`,
|
||||
// because `type[Any]` can materialize to `type[object]`.
|
||||
(Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& self.is_assignable_to(db, KnownClass::Type.to_instance(db)) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
|
||||
self_instance.is_assignable_to(db, target_instance)
|
||||
}
|
||||
|
||||
(Type::Callable(self_callable), Type::Callable(target_callable)) => {
|
||||
self_callable.is_assignable_to(db, target_callable)
|
||||
}
|
||||
|
||||
(Type::NominalInstance(instance), Type::Callable(_))
|
||||
if instance.class.is_subclass_of_any_or_unknown(db) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.is_assignable_to(db, target)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
_ if self
|
||||
.literal_fallback_instance(db)
|
||||
.is_some_and(|instance| instance.is_assignable_to(db, target)) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
|
||||
ClassType::NonGeneric(class_literal)
|
||||
.into_callable(db)
|
||||
.is_assignable_to(db, target)
|
||||
}
|
||||
|
||||
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
|
||||
.into_callable(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
.is_assignable_to(db, target)
|
||||
}
|
||||
|
||||
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
|
||||
.into_callable_type(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.is_assignable_to(db, right)
|
||||
}
|
||||
// Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
|
||||
// a protocol instance can never be assignable to a nominal type,
|
||||
// with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
// TODO other types containing gradual forms
|
||||
_ => self.is_subtype_of(db, target),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is [equivalent to] type `other`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
@@ -7027,6 +6768,45 @@ impl<'db> ConstructorCallError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum TypeRelation {
|
||||
Subtyping,
|
||||
Assignability,
|
||||
}
|
||||
|
||||
impl TypeRelation {
|
||||
/// Non-fully-static types do not participate in subtyping, only assignability,
|
||||
/// so the subtyping relation does not even apply to them.
|
||||
///
|
||||
/// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
|
||||
/// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
|
||||
/// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
|
||||
/// so the question is simply unanswerable for non-fully-static types.
|
||||
///
|
||||
/// However, the assignability relation applies to all types, even non-fully-static ones.
|
||||
fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db),
|
||||
TypeRelation::Assignability => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether `type_1` and `type_2` are equivalent.
|
||||
///
|
||||
/// Depending on whether the context is a subtyping test or an assignability test,
|
||||
/// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`].
|
||||
fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2),
|
||||
TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2),
|
||||
}
|
||||
}
|
||||
|
||||
const fn applies_to_non_fully_static_types(self) -> bool {
|
||||
matches!(self, TypeRelation::Assignability)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Truthiness {
|
||||
/// For an object `x`, `bool(x)` will always return `True`
|
||||
@@ -7139,26 +6919,16 @@ impl<'db> BoundMethodType<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
// A bound method is a typically a subtype of itself. However, we must explicitly verify
|
||||
// the subtyping of the underlying function signatures (since they might be specialized
|
||||
// differently), and of the bound self parameter (taking care that parameters, including a
|
||||
// bound self parameter, are contravariant.)
|
||||
self.function(db).is_subtype_of(db, other.function(db))
|
||||
self.function(db)
|
||||
.has_relation_to(db, other.function(db), relation)
|
||||
&& other
|
||||
.self_instance(db)
|
||||
.is_subtype_of(db, self.self_instance(db))
|
||||
}
|
||||
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A bound method is a typically assignable to itself. However, we must explicitly verify
|
||||
// the assignability of the underlying function signatures (since they might be specialized
|
||||
// differently), and of the bound self parameter (taking care that parameters, including a
|
||||
// bound self parameter, are contravariant.)
|
||||
self.function(db).is_assignable_to(db, other.function(db))
|
||||
&& other
|
||||
.self_instance(db)
|
||||
.is_assignable_to(db, self.self_instance(db))
|
||||
.has_relation_to(db, self.self_instance(db), relation)
|
||||
}
|
||||
|
||||
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
@@ -7276,26 +7046,15 @@ impl<'db> CallableType<'db> {
|
||||
self.signatures(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Check whether this callable type is a subtype of another callable type.
|
||||
/// Check whether this callable type has the given relation to another callable type.
|
||||
///
|
||||
/// See [`Type::is_subtype_of`] for more details.
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_is_function_like = self.is_function_like(db);
|
||||
let other_is_function_like = other.is_function_like(db);
|
||||
(self_is_function_like || !other_is_function_like)
|
||||
&& self.signatures(db).is_subtype_of(db, other.signatures(db))
|
||||
}
|
||||
|
||||
/// Check whether this callable type is assignable to another callable type.
|
||||
///
|
||||
/// See [`Type::is_assignable_to`] for more details.
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_is_function_like = self.is_function_like(db);
|
||||
let other_is_function_like = other.is_function_like(db);
|
||||
(self_is_function_like || !other_is_function_like)
|
||||
&& self
|
||||
.signatures(db)
|
||||
.is_assignable_to(db, other.signatures(db))
|
||||
/// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details.
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
if other.is_function_like(db) && !self.is_function_like(db) {
|
||||
return false;
|
||||
}
|
||||
self.signatures(db)
|
||||
.has_relation_to(db, other.signatures(db), relation)
|
||||
}
|
||||
|
||||
/// Check whether this callable type is equivalent to another callable type.
|
||||
@@ -7348,50 +7107,17 @@ pub enum MethodWrapperKind<'db> {
|
||||
}
|
||||
|
||||
impl<'db> MethodWrapperKind<'db> {
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(other_function),
|
||||
) => self_function.is_subtype_of(db, other_function),
|
||||
) => self_function.has_relation_to(db, other_function, relation),
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderCall(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderCall(other_function),
|
||||
) => self_function.is_subtype_of(db, other_function),
|
||||
|
||||
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
|
||||
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
|
||||
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
|
||||
self == other
|
||||
}
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(other_function),
|
||||
) => self_function.is_assignable_to(db, other_function),
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderCall(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderCall(other_function),
|
||||
) => self_function.is_assignable_to(db, other_function),
|
||||
) => self_function.has_relation_to(db, other_function, relation),
|
||||
|
||||
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
|
||||
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeVarInstance,
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxOrderSet, KnownModule, Program,
|
||||
@@ -29,8 +29,8 @@ use crate::{
|
||||
place_table, semantic_index, use_def_map,
|
||||
},
|
||||
types::{
|
||||
CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType,
|
||||
UnionBuilder, UnionType, definition_expression_type,
|
||||
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder,
|
||||
UnionType, definition_expression_type,
|
||||
},
|
||||
};
|
||||
use indexmap::IndexSet;
|
||||
@@ -340,23 +340,22 @@ impl<'db> ClassType<'db> {
|
||||
class_literal.is_final(db)
|
||||
}
|
||||
|
||||
/// Is this class a subclass of `Any` or `Unknown`?
|
||||
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
matches!(
|
||||
base,
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.has_relation_to(db, other, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate.
|
||||
ClassBase::Dynamic(_) => false,
|
||||
ClassBase::Dynamic(_) => {
|
||||
relation.applies_to_non_fully_static_types() && !other.is_final(db)
|
||||
}
|
||||
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic => false,
|
||||
@@ -365,9 +364,11 @@ impl<'db> ClassType<'db> {
|
||||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_subtype_of(db, other.specialization(db))
|
||||
&& base.specialization(db).has_relation_to(
|
||||
db,
|
||||
other.specialization(db),
|
||||
relation,
|
||||
)
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
@@ -390,30 +391,6 @@ impl<'db> ClassType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db),
|
||||
ClassBase::Dynamic(_) => false,
|
||||
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic => false,
|
||||
|
||||
ClassBase::Class(base) => match (base, other) {
|
||||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_assignable_to(db, other.specialization(db))
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
match (self, other) {
|
||||
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
|
||||
|
||||
@@ -279,10 +279,6 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_dynamic(self) -> bool {
|
||||
matches!(self, Self::Dynamic(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassType<'db>> for ClassBase<'db> {
|
||||
|
||||
@@ -1878,11 +1878,14 @@ pub(crate) fn report_invalid_arguments_to_callable(
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn add_type_expression_reference_link(mut diag: LintDiagnosticGuard) {
|
||||
pub(crate) fn add_type_expression_reference_link<'db, 'ctx>(
|
||||
mut diag: LintDiagnosticGuard<'db, 'ctx>,
|
||||
) -> LintDiagnosticGuard<'db, 'ctx> {
|
||||
diag.info("See the following page for a reference on valid type expressions:");
|
||||
diag.info(
|
||||
"https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions",
|
||||
);
|
||||
diag
|
||||
}
|
||||
|
||||
pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol(
|
||||
|
||||
@@ -67,7 +67,9 @@ use crate::semantic_index::semantic_index;
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::narrow::ClassInfoConstraintFunction;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
use crate::types::{BoundMethodType, CallableType, Type, TypeMapping, TypeVarInstance};
|
||||
use crate::types::{
|
||||
BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
/// A collection of useful spans for annotating functions.
|
||||
@@ -707,6 +709,18 @@ impl<'db> FunctionType<'db> {
|
||||
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match relation {
|
||||
TypeRelation::Subtyping => self.is_subtype_of(db, other),
|
||||
TypeRelation::Assignability => self.is_assignable_to(db, other),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function type is the subtype of itself, and not of any other function type. However,
|
||||
// our representation of a function type includes any specialization that should be applied
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::types::class_base::ClassBase;
|
||||
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
|
||||
use crate::types::signatures::{Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
KnownInstanceType, Type, TypeMapping, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
TypeVarVariance, UnionType, declaration_type, todo_type,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
@@ -358,7 +358,12 @@ impl<'db> Specialization<'db> {
|
||||
Self::new(db, self.generic_context(db), types)
|
||||
}
|
||||
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
let generic_context = self.generic_context(db);
|
||||
if generic_context != other.generic_context(db) {
|
||||
return false;
|
||||
@@ -368,20 +373,31 @@ impl<'db> Specialization<'db> {
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
|
||||
return false;
|
||||
if self_type.is_dynamic() || other_type.is_dynamic() {
|
||||
match relation {
|
||||
TypeRelation::Assignability => continue,
|
||||
TypeRelation::Subtyping => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// Subtyping of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// Subtyping/assignability of each type in the specialization depends on the variance
|
||||
// of the corresponding typevar:
|
||||
// - covariant: verify that self_type <: other_type
|
||||
// - contravariant: verify that other_type <: self_type
|
||||
// - invariant: verify that self_type == other_type
|
||||
// - bivariant: skip, can't make subtyping false
|
||||
// - invariant: verify that self_type <: other_type AND other_type <: self_type
|
||||
// - bivariant: skip, can't make subtyping/assignability false
|
||||
let compatible = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant => self_type.is_equivalent_to(db, *other_type),
|
||||
TypeVarVariance::Covariant => self_type.is_subtype_of(db, *other_type),
|
||||
TypeVarVariance::Contravariant => other_type.is_subtype_of(db, *self_type),
|
||||
TypeVarVariance::Invariant => match relation {
|
||||
TypeRelation::Subtyping => self_type.is_equivalent_to(db, *other_type),
|
||||
TypeRelation::Assignability => {
|
||||
self_type.is_assignable_to(db, *other_type)
|
||||
&& other_type.is_assignable_to(db, *self_type)
|
||||
}
|
||||
},
|
||||
TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation),
|
||||
TypeVarVariance::Contravariant => {
|
||||
other_type.has_relation_to(db, *self_type, relation)
|
||||
}
|
||||
TypeVarVariance::Bivariant => true,
|
||||
};
|
||||
if !compatible {
|
||||
@@ -426,43 +442,6 @@ impl<'db> Specialization<'db> {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
|
||||
let generic_context = self.generic_context(db);
|
||||
if generic_context != other.generic_context(db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assignability of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// - covariant: verify that self_type <: other_type
|
||||
// - contravariant: verify that other_type <: self_type
|
||||
// - invariant: verify that self_type <: other_type AND other_type <: self_type
|
||||
// - bivariant: skip, can't make assignability false
|
||||
let compatible = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant => {
|
||||
self_type.is_assignable_to(db, *other_type)
|
||||
&& other_type.is_assignable_to(db, *self_type)
|
||||
}
|
||||
TypeVarVariance::Covariant => self_type.is_assignable_to(db, *other_type),
|
||||
TypeVarVariance::Contravariant => other_type.is_assignable_to(db, *self_type),
|
||||
TypeVarVariance::Bivariant => true,
|
||||
};
|
||||
if !compatible {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
//! the query cycle until a fixed-point is reached. Salsa has a built-in fixed limit on the number
|
||||
//! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course
|
||||
//! be considered a bug.)
|
||||
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
@@ -95,11 +96,11 @@ use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||
DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass,
|
||||
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
||||
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness,
|
||||
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
|
||||
UnionType, binding_type, todo_type,
|
||||
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
|
||||
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType,
|
||||
SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
|
||||
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
@@ -8308,7 +8309,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.store_expression_type(slice, inner_annotation_ty.inner_type());
|
||||
inner_annotation_ty
|
||||
} else {
|
||||
self.infer_type_expression(slice);
|
||||
for argument in arguments {
|
||||
self.infer_expression(argument);
|
||||
}
|
||||
self.store_expression_type(slice, Type::unknown());
|
||||
TypeAndQualifiers::unknown()
|
||||
}
|
||||
} else {
|
||||
@@ -8416,15 +8420,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
|
||||
fn report_invalid_type_expression(
|
||||
&mut self,
|
||||
&self,
|
||||
expression: &ast::Expr,
|
||||
message: std::fmt::Arguments,
|
||||
) -> Type<'db> {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, expression) {
|
||||
let diag = builder.into_diagnostic(message);
|
||||
diagnostic::add_type_expression_reference_link(diag);
|
||||
}
|
||||
Type::unknown()
|
||||
) -> Option<LintDiagnosticGuard> {
|
||||
self.context
|
||||
.report_lint(&INVALID_TYPE_FORM, expression)
|
||||
.map(|builder| {
|
||||
diagnostic::add_type_expression_reference_link(builder.into_diagnostic(message))
|
||||
})
|
||||
}
|
||||
|
||||
/// Infer the type of a type expression without storing the result.
|
||||
@@ -8511,56 +8515,126 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
|
||||
// TODO: add a subdiagnostic linking to type-expression grammar
|
||||
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
|
||||
ast::Expr::BytesLiteral(_) => self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Bytes literals are not allowed in this context in a type expression"),
|
||||
),
|
||||
ast::Expr::BytesLiteral(_) => {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Bytes literals are not allowed in this context in a type expression"
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
// TODO: add a subdiagnostic linking to type-expression grammar
|
||||
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
}) => self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Int literals are not allowed in this context in a type expression"),
|
||||
),
|
||||
}) => {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Int literals are not allowed in this context in a type expression"
|
||||
),
|
||||
);
|
||||
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Float(_),
|
||||
..
|
||||
}) => self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Float literals are not allowed in type expressions"),
|
||||
),
|
||||
}) => {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Float literals are not allowed in type expressions"),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Complex { .. },
|
||||
..
|
||||
}) => self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Complex literals are not allowed in type expressions"),
|
||||
),
|
||||
|
||||
// TODO: add a subdiagnostic linking to type-expression grammar
|
||||
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
|
||||
ast::Expr::BooleanLiteral(_) => self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Boolean literals are not allowed in this context in a type expression"
|
||||
),
|
||||
),
|
||||
|
||||
// TODO: add a subdiagnostic linking to type-expression grammar
|
||||
// and stating that it is only valid as first argument to `typing.Callable[]`
|
||||
ast::Expr::List(list) => {
|
||||
self.infer_list_expression(list);
|
||||
}) => {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Complex literals are not allowed in type expressions"),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::BooleanLiteral(_) => {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Boolean literals are not allowed in this context in a type expression"
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::List(list) => {
|
||||
let db = self.db();
|
||||
|
||||
let inner_types: Vec<Type<'db>> = list
|
||||
.iter()
|
||||
.map(|element| self.infer_type_expression(element))
|
||||
.collect();
|
||||
|
||||
if let Some(mut diagnostic) = self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"List literals are not allowed in this context in a type expression"
|
||||
),
|
||||
)
|
||||
) {
|
||||
if !inner_types.iter().any(|ty| {
|
||||
matches!(
|
||||
ty,
|
||||
Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown)
|
||||
)
|
||||
}) {
|
||||
let hinted_type = if list.len() == 1 {
|
||||
KnownClass::List.to_specialized_instance(db, inner_types)
|
||||
} else {
|
||||
TupleType::from_elements(db, inner_types)
|
||||
};
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Did you mean `{}`?",
|
||||
hinted_type.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Tuple(tuple) => {
|
||||
let inner_types: Vec<Type<'db>> = tuple
|
||||
.elts
|
||||
.iter()
|
||||
.map(|expr| self.infer_type_expression(expr))
|
||||
.collect();
|
||||
|
||||
if tuple.parenthesized {
|
||||
if let Some(mut diagnostic) = self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Tuple literals are not allowed in this context in a type expression"
|
||||
),
|
||||
) {
|
||||
if !inner_types.iter().any(|ty| {
|
||||
matches!(
|
||||
ty,
|
||||
Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown)
|
||||
)
|
||||
}) {
|
||||
let hinted_type = TupleType::from_elements(self.db(), inner_types);
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Did you mean `{}`?",
|
||||
hinted_type.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::BoolOp(bool_op) => {
|
||||
@@ -8568,7 +8642,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Boolean operations are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Named(named) => {
|
||||
@@ -8576,7 +8651,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Named expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::UnaryOp(unary) => {
|
||||
@@ -8584,7 +8660,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Unary operations are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Lambda(lambda_expression) => {
|
||||
@@ -8592,7 +8669,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("`lambda` expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::If(if_expression) => {
|
||||
@@ -8600,7 +8678,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("`if` expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Dict(dict) => {
|
||||
@@ -8608,7 +8687,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Dict literals are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Set(set) => {
|
||||
@@ -8616,7 +8696,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Set literals are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::DictComp(dictcomp) => {
|
||||
@@ -8624,7 +8705,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Dict comprehensions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::ListComp(listcomp) => {
|
||||
@@ -8632,7 +8714,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("List comprehensions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::SetComp(setcomp) => {
|
||||
@@ -8640,7 +8723,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Set comprehensions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Generator(generator) => {
|
||||
@@ -8648,7 +8732,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Generator expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Await(await_expression) => {
|
||||
@@ -8656,7 +8741,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("`await` expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Yield(yield_expression) => {
|
||||
@@ -8664,7 +8750,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("`yield` expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::YieldFrom(yield_from) => {
|
||||
@@ -8672,7 +8759,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("`yield from` expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Compare(compare) => {
|
||||
@@ -8680,7 +8768,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Comparison expressions are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Call(call_expr) => {
|
||||
@@ -8688,7 +8777,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Function calls are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
@@ -8696,7 +8786,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("F-strings are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::TString(tstring) => {
|
||||
@@ -8704,7 +8795,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("T-strings are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Slice(slice) => {
|
||||
@@ -8712,7 +8804,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!("Slices are not allowed in type expressions"),
|
||||
)
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
@@ -8724,11 +8817,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
todo_type!("ellipsis literal in type expression")
|
||||
}
|
||||
|
||||
ast::Expr::Tuple(tuple) => {
|
||||
self.infer_tuple_expression(tuple);
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Starred(starred) => {
|
||||
self.infer_starred_expression(starred);
|
||||
todo_type!("PEP 646")
|
||||
@@ -9076,7 +9164,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
|
||||
let [type_expr, metadata @ ..] = &arguments[..] else {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
for argument in arguments {
|
||||
self.infer_expression(argument);
|
||||
}
|
||||
self.store_expression_type(arguments_slice, Type::unknown());
|
||||
return Type::unknown();
|
||||
};
|
||||
|
||||
@@ -9468,18 +9559,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
||||
let value_ty = self.infer_expression(value);
|
||||
// TODO: Check that value type is enum otherwise return None
|
||||
value_ty
|
||||
let ty = value_ty
|
||||
.member(self.db(), &attr.id)
|
||||
.place
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::unknown())
|
||||
.unwrap_or(Type::unknown());
|
||||
self.store_expression_type(parameters, ty);
|
||||
ty
|
||||
}
|
||||
// for negative and positive numbers
|
||||
ast::Expr::UnaryOp(u)
|
||||
if matches!(u.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd)
|
||||
&& u.operand.is_number_literal_expr() =>
|
||||
{
|
||||
self.infer_unary_expression(u)
|
||||
let ty = self.infer_unary_expression(u);
|
||||
self.store_expression_type(parameters, ty);
|
||||
ty
|
||||
}
|
||||
_ => {
|
||||
self.infer_expression(parameters);
|
||||
|
||||
@@ -5,14 +5,16 @@ use std::marker::PhantomData;
|
||||
use super::protocol_class::ProtocolInterface;
|
||||
use super::{ClassType, KnownClass, SubclassOfType, Type};
|
||||
use crate::place::{Boundness, Place, PlaceAndQualifiers};
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance};
|
||||
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
pub(super) use synthesized_protocol::SynthesizedProtocolType;
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
|
||||
if class.class_literal(db).0.is_protocol(db) {
|
||||
if class.is_known(db, KnownClass::Any) {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
} else if class.class_literal(db).0.is_protocol(db) {
|
||||
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
|
||||
} else {
|
||||
Self::NominalInstance(NominalInstanceType::from_class(class))
|
||||
@@ -78,19 +80,19 @@ impl<'db> NominalInstanceType<'db> {
|
||||
Self::from_class(self.class.normalized(db))
|
||||
}
|
||||
|
||||
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// N.B. The subclass relation is fully static
|
||||
self.class.is_subclass_of(db, other.class)
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
self.class.has_relation_to(db, other.class, relation)
|
||||
}
|
||||
|
||||
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.class.is_equivalent_to(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.class.is_assignable_to(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
|
||||
return true;
|
||||
@@ -254,16 +256,20 @@ impl<'db> ProtocolInstanceType<'db> {
|
||||
self.inner.interface(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is a subtype of the protocol `other`.
|
||||
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is assignable to the protocol `other`.
|
||||
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
other
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
relation.applies_to(
|
||||
db,
|
||||
Type::ProtocolInstance(self),
|
||||
Type::ProtocolInstance(other),
|
||||
) && other
|
||||
.inner
|
||||
.interface(db)
|
||||
.is_sub_interface_of(db, self.inner.interface(db))
|
||||
|
||||
@@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
|
||||
use super::{DynamicType, Type, definition_expression_type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance, todo_type};
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
@@ -98,11 +98,23 @@ impl<'db> CallableSignature<'db> {
|
||||
.all(|signature| signature.is_fully_static(db))
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
other: &Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match relation {
|
||||
TypeRelation::Subtyping => self.is_subtype_of(db, other),
|
||||
TypeRelation::Assignability => self.is_assignable_to(db, other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this callable type is a subtype of another callable type.
|
||||
///
|
||||
/// See [`Type::is_subtype_of`] for more details.
|
||||
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature),
|
||||
@@ -113,7 +125,7 @@ impl<'db> CallableSignature<'db> {
|
||||
///
|
||||
/// See [`Type::is_assignable_to`] for more details.
|
||||
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature),
|
||||
@@ -124,7 +136,7 @@ impl<'db> CallableSignature<'db> {
|
||||
/// types.
|
||||
///
|
||||
/// The `check_signature` closure is used to check the relation between two [`Signature`]s.
|
||||
fn is_assignable_to_impl<F>(
|
||||
fn has_relation_to_impl<F>(
|
||||
self_signatures: &[Signature<'db>],
|
||||
other_signatures: &[Signature<'db>],
|
||||
check_signature: &F,
|
||||
@@ -140,7 +152,7 @@ impl<'db> CallableSignature<'db> {
|
||||
|
||||
// `self` is possibly overloaded while `other` is definitely not overloaded.
|
||||
(_, [_]) => self_signatures.iter().any(|self_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
std::slice::from_ref(self_signature),
|
||||
other_signatures,
|
||||
check_signature,
|
||||
@@ -149,7 +161,7 @@ impl<'db> CallableSignature<'db> {
|
||||
|
||||
// `self` is definitely not overloaded while `other` is possibly overloaded.
|
||||
([_], _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
@@ -158,7 +170,7 @@ impl<'db> CallableSignature<'db> {
|
||||
|
||||
// `self` is definitely overloaded while `other` is possibly overloaded.
|
||||
(_, _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::place::PlaceAndQualifiers;
|
||||
use crate::types::{
|
||||
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeVarInstance,
|
||||
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation,
|
||||
TypeVarInstance,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
@@ -30,10 +31,14 @@ impl<'db> SubclassOfType<'db> {
|
||||
SubclassOfInner::Class(class) => {
|
||||
if class.is_final(db) {
|
||||
Type::from(class)
|
||||
} else if class.is_object(db) {
|
||||
KnownClass::Type.to_instance(db)
|
||||
} else {
|
||||
Type::SubclassOf(Self { subclass_of })
|
||||
match class.known(db) {
|
||||
Some(KnownClass::Object) => KnownClass::Type.to_instance(db),
|
||||
Some(KnownClass::Any) => Type::SubclassOf(Self {
|
||||
subclass_of: SubclassOfInner::Dynamic(DynamicType::Any),
|
||||
}),
|
||||
_ => Type::SubclassOf(Self { subclass_of }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,21 +108,23 @@ impl<'db> SubclassOfType<'db> {
|
||||
Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
///
|
||||
/// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant;
|
||||
/// only fully static types participate in subtyping.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
|
||||
/// Return `true` if `self` has a certain relation to `other`.
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: SubclassOfType<'db>,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
// Non-fully-static types do not participate in subtyping
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => {
|
||||
relation.applies_to_non_fully_static_types()
|
||||
}
|
||||
|
||||
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
|
||||
// and `type[int]` describes all possible runtime subclasses of the class `int`.
|
||||
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
|
||||
(SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => {
|
||||
// N.B. The subclass relation is fully static
|
||||
self_class.is_subclass_of(db, other_class)
|
||||
self_class.has_relation_to(db, other_class, relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,10 @@ impl System for LSPSystem {
|
||||
fn case_sensitivity(&self) -> CaseSensitivity {
|
||||
self.os_system.case_sensitivity()
|
||||
}
|
||||
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
self.os_system.env_var(name)
|
||||
}
|
||||
}
|
||||
|
||||
fn not_a_text_document(path: impl Display) -> std::io::Error {
|
||||
|
||||
Reference in New Issue
Block a user