mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-10 05:39:12 +00:00
Support IfExp
with dual string arms in invalid-envvar-value
(#6538)
## Summary Closes https://github.com/astral-sh/ruff/issues/6537. We need to improve the `PythonType` algorithm, so this also documents some of its limitations as TODOs.
This commit is contained in:
parent
8660e5057c
commit
446ceed1ad
6 changed files with 52 additions and 78 deletions
|
@ -10,3 +10,4 @@ os.getenv("AA", "GOOD" + "BAD")
|
||||||
os.getenv("AA", "GOOD" + 1)
|
os.getenv("AA", "GOOD" + 1)
|
||||||
os.getenv("AA", "GOOD %s" % "BAD")
|
os.getenv("AA", "GOOD %s" % "BAD")
|
||||||
os.getenv("B", Z)
|
os.getenv("B", Z)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ os.getenv(key="foo", default="bar")
|
||||||
os.getenv(key=f"foo", default="bar")
|
os.getenv(key=f"foo", default="bar")
|
||||||
os.getenv(key="foo" + "bar", default=1)
|
os.getenv(key="foo" + "bar", default=1)
|
||||||
os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
|
os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
|
||||||
|
os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
|
||||||
|
os.getenv(1 if using_clear_path else "PATH_ORIG")
|
||||||
|
|
||||||
AA = "aa"
|
AA = "aa"
|
||||||
os.getenv(AA)
|
os.getenv(AA)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::{self as ast, Constant, Expr, Operator, Ranged};
|
use ruff_python_ast::{self as ast, Ranged};
|
||||||
|
use ruff_python_semantic::analyze::type_inference::PythonType;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
@ -32,46 +33,6 @@ impl Violation for InvalidEnvvarValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_key(expr: &Expr) -> bool {
|
|
||||||
// We can't infer the types of these defaults, so assume they're valid.
|
|
||||||
if matches!(
|
|
||||||
expr,
|
|
||||||
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow string concatenation.
|
|
||||||
if let Expr::BinOp(ast::ExprBinOp {
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
op: Operator::Add,
|
|
||||||
range: _,
|
|
||||||
}) = expr
|
|
||||||
{
|
|
||||||
return is_valid_key(left) && is_valid_key(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow string formatting.
|
|
||||||
if let Expr::BinOp(ast::ExprBinOp {
|
|
||||||
left,
|
|
||||||
op: Operator::Mod,
|
|
||||||
..
|
|
||||||
}) = expr
|
|
||||||
{
|
|
||||||
return is_valid_key(left);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, the default must be a string.
|
|
||||||
matches!(
|
|
||||||
expr,
|
|
||||||
Expr::Constant(ast::ExprConstant {
|
|
||||||
value: Constant::Str { .. },
|
|
||||||
..
|
|
||||||
}) | Expr::FString(_)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PLE1507
|
/// PLE1507
|
||||||
pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall) {
|
pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall) {
|
||||||
if checker
|
if checker
|
||||||
|
@ -84,10 +45,15 @@ pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall)
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_valid_key(expr) {
|
if matches!(
|
||||||
checker
|
PythonType::from(expr),
|
||||||
.diagnostics
|
PythonType::String | PythonType::Unknown
|
||||||
.push(Diagnostic::new(InvalidEnvvarValue, expr.range()));
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checker
|
||||||
|
.diagnostics
|
||||||
|
.push(Diagnostic::new(InvalidEnvvarValue, expr.range()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt
|
||||||
|
|
||||||
for stmt in returns {
|
for stmt in returns {
|
||||||
if let Some(value) = stmt.value.as_deref() {
|
if let Some(value) = stmt.value.as_deref() {
|
||||||
// Disallow other, non-
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
PythonType::from(value),
|
PythonType::from(value),
|
||||||
PythonType::String | PythonType::Unknown
|
PythonType::String | PythonType::Unknown
|
||||||
|
|
|
@ -31,14 +31,4 @@ invalid_envvar_value.py:8:11: PLE1507 Invalid type for initial `os.getenv` argum
|
||||||
10 | os.getenv(key=f"foo", default="bar")
|
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 |
|
|
||||||
14 | AA = "aa"
|
|
||||||
|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! Analysis rules to perform basic type inference on individual expressions.
|
//! Analysis rules to perform basic type inference on individual expressions.
|
||||||
|
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::{Constant, Expr};
|
use ruff_python_ast::{Constant, Expr, Operator};
|
||||||
|
|
||||||
/// An extremely simple type inference system for individual expressions.
|
/// An extremely simple type inference system for individual expressions.
|
||||||
///
|
///
|
||||||
|
@ -9,7 +9,7 @@ use ruff_python_ast::{Constant, Expr};
|
||||||
/// such as strings, integers, floats, and containers. It cannot infer the
|
/// such as strings, integers, floats, and containers. It cannot infer the
|
||||||
/// types of variables or expressions that are not statically known from
|
/// types of variables or expressions that are not statically known from
|
||||||
/// individual AST nodes alone.
|
/// individual AST nodes alone.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, is_macro::Is)]
|
||||||
pub enum PythonType {
|
pub enum PythonType {
|
||||||
/// A string literal, such as `"hello"`.
|
/// A string literal, such as `"hello"`.
|
||||||
String,
|
String,
|
||||||
|
@ -55,27 +55,43 @@ impl From<&Expr> for PythonType {
|
||||||
Expr::Tuple(_) => PythonType::Tuple,
|
Expr::Tuple(_) => PythonType::Tuple,
|
||||||
Expr::GeneratorExp(_) => PythonType::Generator,
|
Expr::GeneratorExp(_) => PythonType::Generator,
|
||||||
Expr::FString(_) => PythonType::String,
|
Expr::FString(_) => PythonType::String,
|
||||||
Expr::BinOp(ast::ExprBinOp { left, op, .. }) => {
|
Expr::IfExp(ast::ExprIfExp { body, orelse, .. }) => {
|
||||||
// Ex) "a" % "b"
|
let body = PythonType::from(body.as_ref());
|
||||||
if op.is_mod() {
|
let orelse = PythonType::from(orelse.as_ref());
|
||||||
if matches!(
|
// TODO(charlie): If we have two known types, we should return a union. As-is,
|
||||||
left.as_ref(),
|
// callers that ignore the `Unknown` type will allow invalid expressions (e.g.,
|
||||||
Expr::Constant(ast::ExprConstant {
|
// if you're testing for strings, you may accept `String` or `Unknown`, and you'd
|
||||||
value: Constant::Str(..),
|
// now accept, e.g., `1 if True else "a"`, which resolves to `Unknown`).
|
||||||
..
|
if body == orelse {
|
||||||
})
|
body
|
||||||
) {
|
} else {
|
||||||
return PythonType::String;
|
PythonType::Unknown
|
||||||
}
|
}
|
||||||
if matches!(
|
}
|
||||||
left.as_ref(),
|
Expr::BinOp(ast::ExprBinOp {
|
||||||
Expr::Constant(ast::ExprConstant {
|
left, op, right, ..
|
||||||
value: Constant::Bytes(..),
|
}) => {
|
||||||
..
|
match op {
|
||||||
})
|
// Ex) "a" + "b"
|
||||||
) {
|
Operator::Add => {
|
||||||
return PythonType::Bytes;
|
match (
|
||||||
|
PythonType::from(left.as_ref()),
|
||||||
|
PythonType::from(right.as_ref()),
|
||||||
|
) {
|
||||||
|
(PythonType::String, PythonType::String) => return PythonType::String,
|
||||||
|
(PythonType::Bytes, PythonType::Bytes) => return PythonType::Bytes,
|
||||||
|
// TODO(charlie): If we have two known types, they may be incompatible.
|
||||||
|
// Return an error (e.g., for `1 + "a"`).
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Ex) "a" % "b"
|
||||||
|
Operator::Mod => match PythonType::from(left.as_ref()) {
|
||||||
|
PythonType::String => return PythonType::String,
|
||||||
|
PythonType::Bytes => return PythonType::Bytes,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
PythonType::Unknown
|
PythonType::Unknown
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue