Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for unions to our Python builtins type system #6541

Merged
merged 1 commit into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"%(key)d" % {"key": []}
print("%d" % ("%s" % ("nested",),))
"%d" % ((1, 2, 3),)
"%d" % (1 if x > 0 else [])

# False negatives
WORD = "abc"
Expand Down Expand Up @@ -55,3 +56,4 @@
"%d" % (len(foo),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )
"%d" % (1 if x > 0 else 2)
34 changes: 20 additions & 14 deletions crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_semantic::analyze::type_inference::PythonType;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};

use crate::checkers::ast::Checker;

Expand Down Expand Up @@ -59,14 +59,16 @@ impl FormatType {
| PythonType::Set
| PythonType::Tuple
| PythonType::Generator
| PythonType::Complex
| PythonType::Bool
| PythonType::Ellipsis
| PythonType::None => matches!(
self,
FormatType::Unknown | FormatType::String | FormatType::Repr
),
PythonType::Integer => matches!(
PythonType::Number(NumberLike::Complex | NumberLike::Bool) => matches!(
self,
FormatType::Unknown | FormatType::String | FormatType::Repr
),
PythonType::Number(NumberLike::Integer) => matches!(
self,
FormatType::Unknown
| FormatType::String
Expand All @@ -75,15 +77,14 @@ impl FormatType {
| FormatType::Float
| FormatType::Number
),
PythonType::Float => matches!(
PythonType::Number(NumberLike::Float) => matches!(
self,
FormatType::Unknown
| FormatType::String
| FormatType::Repr
| FormatType::Float
| FormatType::Number
),
PythonType::Unknown => true,
}
}
}
Expand Down Expand Up @@ -118,16 +119,22 @@ fn collect_specs(formats: &[CFormatStrOrBytes<String>]) -> Vec<&CFormatSpec> {

/// Return `true` if the format string is equivalent to the constant type
fn equivalent(format: &CFormatSpec, value: &Expr) -> bool {
let format: FormatType = format.format_char.into();
let constant: PythonType = value.into();
format.is_compatible_with(constant)
let format = FormatType::from(format.format_char);
match ResolvedPythonType::from(value) {
ResolvedPythonType::Atom(atom) => format.is_compatible_with(atom),
ResolvedPythonType::Union(atoms) => {
atoms.iter().all(|atom| format.is_compatible_with(*atom))
}
ResolvedPythonType::Unknown => true,
ResolvedPythonType::TypeError => true,
}
}

/// Return `true` if the [`Constnat`] aligns with the format type.
/// Return `true` if the [`Constant`] aligns with the format type.
fn is_valid_constant(formats: &[CFormatStrOrBytes<String>], value: &Expr) -> bool {
let formats = collect_specs(formats);
// If there is more than one format, this is not valid python and we should
// return true so that no error is reported
// If there is more than one format, this is not valid Python and we should
// return true so that no error is reported.
let [format] = formats.as_slice() else {
return true;
};
Expand Down Expand Up @@ -242,8 +249,7 @@ pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right:
values,
range: _,
}) => is_valid_dict(&format_strings, keys, values),
Expr::Constant(_) => is_valid_constant(&format_strings, right),
_ => true,
_ => is_valid_constant(&format_strings, right),
};
if !is_valid {
checker
Expand Down
6 changes: 3 additions & 3 deletions crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Ranged};
use ruff_python_semantic::analyze::type_inference::PythonType;
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};

use crate::checkers::ast::Checker;

Expand Down Expand Up @@ -46,8 +46,8 @@ pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall)
};

if matches!(
PythonType::from(expr),
PythonType::String | PythonType::Unknown
ResolvedPythonType::from(expr),
ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::String)
) {
return;
}
Expand Down
6 changes: 3 additions & 3 deletions crates/ruff/src/rules/pylint/rules/invalid_str_return.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use ruff_python_ast::{Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{helpers::ReturnStatementVisitor, statement_visitor::StatementVisitor};
use ruff_python_semantic::analyze::type_inference::PythonType;
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};

use crate::checkers::ast::Checker;

Expand Down Expand Up @@ -42,8 +42,8 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(
PythonType::from(value),
PythonType::String | PythonType::Unknown
ResolvedPythonType::from(value),
ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::String)
) {
checker
.diagnostics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ bad_string_format_type.py:10:1: PLE1307 Format type does not match argument type
12 | "%d" % ([],)
|

bad_string_format_type.py:11:1: PLE1307 Format type does not match argument type
|
9 | "%x" % 1.1
10 | "%(key)x" % {"key": 1.1}
11 | "%d" % []
| ^^^^^^^^^ PLE1307
12 | "%d" % ([],)
13 | "%(key)d" % {"key": []}
|

bad_string_format_type.py:12:1: PLE1307 Format type does not match argument type
|
10 | "%(key)x" % {"key": 1.1}
Expand Down Expand Up @@ -96,6 +106,7 @@ bad_string_format_type.py:14:7: PLE1307 Format type does not match argument type
14 | print("%d" % ("%s" % ("nested",),))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1307
15 | "%d" % ((1, 2, 3),)
16 | "%d" % (1 if x > 0 else [])
|

bad_string_format_type.py:15:1: PLE1307 Format type does not match argument type
Expand All @@ -104,8 +115,17 @@ bad_string_format_type.py:15:1: PLE1307 Format type does not match argument type
14 | print("%d" % ("%s" % ("nested",),))
15 | "%d" % ((1, 2, 3),)
| ^^^^^^^^^^^^^^^^^^^ PLE1307
16 |
17 | # False negatives
16 | "%d" % (1 if x > 0 else [])
|

bad_string_format_type.py:16:1: PLE1307 Format type does not match argument type
|
14 | print("%d" % ("%s" % ("nested",),))
15 | "%d" % ((1, 2, 3),)
16 | "%d" % (1 if x > 0 else [])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1307
17 |
18 | # False negatives
|


Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,24 @@ invalid_envvar_value.py:8:11: PLE1507 Invalid type for initial `os.getenv` argum
10 | os.getenv(key=f"foo", default="bar")
|

invalid_envvar_value.py:12:15: PLE1507 Invalid type for initial `os.getenv` argument; expected `str`
|
10 | os.getenv(key=f"foo", default="bar")
11 | os.getenv(key="foo" + "bar", default=1)
12 | os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
| ^^^^^^^^^ PLE1507
13 | os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
14 | os.getenv(1 if using_clear_path else "PATH_ORIG")
|

invalid_envvar_value.py:14:11: PLE1507 Invalid type for initial `os.getenv` argument; expected `str`
|
12 | os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
13 | os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
14 | os.getenv(1 if using_clear_path else "PATH_ORIG")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1507
15 |
16 | AA = "aa"
|


5 changes: 3 additions & 2 deletions crates/ruff_python_semantic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ bitflags = { workspace = true }
is-macro = { workspace = true }
num-traits = { workspace = true }
rustc-hash = { workspace = true }


smallvec = { workspace = true }

[dev-dependencies]
ruff_python_parser = { path = "../ruff_python_parser" }
Loading