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:
Charlie Marsh 2023-08-13 15:52:10 -04:00 committed by GitHub
parent 8660e5057c
commit 446ceed1ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 78 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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()));
} }
} }

View file

@ -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

View file

@ -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"
|

View file

@ -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
} }