Skip to content
Closed
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
46 changes: 46 additions & 0 deletions crates/pyrefly_python/src/sys_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,52 @@ impl SysInfo {
Some(self.evaluate(x)?.to_bool())
}

/// Return true/false only when evaluation depends on the configured Python environment.
pub fn evaluate_bool_with_sys_info(&self, x: &Expr) -> Option<bool> {
Self::depends_on_sys_info(x).then(|| self.evaluate_bool(x))?
}

fn depends_on_sys_info(x: &Expr) -> bool {
match x {
Expr::Compare(x) => {
Self::depends_on_sys_info(&x.left)
|| x.comparators.iter().any(Self::depends_on_sys_info)
}
Expr::Attribute(ExprAttribute { value, attr, .. }) => {
matches!(
&**value,
Expr::Name(name)
if (&name.id == "sys" && matches!(attr.as_str(), "platform" | "version_info"))
|| (&name.id == "os" && attr.as_str() == "name")
|| Self::is_type_checking_constant_name(attr.as_str())
) || Self::depends_on_sys_info(value)
}
Expr::Name(name) => Self::is_type_checking_constant_name(name.id()),
Expr::Call(x) => {
Self::depends_on_sys_info(&x.func)
|| x.arguments.args.iter().any(Self::depends_on_sys_info)
|| x.arguments
.keywords
.iter()
.any(|x| Self::depends_on_sys_info(&x.value))
}
Expr::Subscript(x) => {
Self::depends_on_sys_info(&x.value) || Self::depends_on_sys_info(&x.slice)
}
Expr::Tuple(x) => x.elts.iter().any(Self::depends_on_sys_info),
Expr::List(x) => x.elts.iter().any(Self::depends_on_sys_info),
Expr::Set(x) => x.elts.iter().any(Self::depends_on_sys_info),
Expr::BoolOp(x) => x.values.iter().any(Self::depends_on_sys_info),
Expr::UnaryOp(x) => Self::depends_on_sys_info(&x.operand),
Expr::Slice(x) => {
x.lower.as_deref().is_some_and(Self::depends_on_sys_info)
|| x.upper.as_deref().is_some_and(Self::depends_on_sys_info)
|| x.step.as_deref().is_some_and(Self::depends_on_sys_info)
}
_ => false,
}
}

fn is_type_checking_constant_name(x: &str) -> bool {
x == "TYPE_CHECKING" || x == "TYPE_CHECKING_WITH_PYREFLY"
}
Expand Down
22 changes: 16 additions & 6 deletions pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,14 +496,24 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
}
Expr::If(x) => {
let condition_type = self.expr_infer(&x.test, errors);
let body_type = self.expr_infer_impl(&x.body, hint, errors).into_ty();
let orelse_type = self.expr_infer_impl(&x.orelse, hint, errors).into_ty();
self.check_dunder_bool_is_callable(&condition_type, x.range(), errors);
self.check_redundant_condition(&condition_type, x.range(), errors);
match self.as_bool(&condition_type, x.test.range(), errors) {
Some(true) => body_type,
Some(false) => orelse_type,
None => self.union(body_type, orelse_type),
match self
.bindings()
.sys_info()
.evaluate_bool_with_sys_info(&x.test)
{
Some(true) => self.expr_infer_impl(&x.body, hint, errors).into_ty(),
Some(false) => self.expr_infer_impl(&x.orelse, hint, errors).into_ty(),
None => {
let body_type = self.expr_infer_impl(&x.body, hint, errors).into_ty();
let orelse_type = self.expr_infer_impl(&x.orelse, hint, errors).into_ty();
match self.as_bool(&condition_type, x.test.range(), errors) {
Some(true) => body_type,
Some(false) => orelse_type,
None => self.union(body_type, orelse_type),
}
}
}
}
Expr::BoolOp(x) => self.boolop(&x.values, x.op, hint, errors),
Expand Down
7 changes: 7 additions & 0 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ table! {
#[derive(Clone, Debug)]
struct BindingsInner {
module_info: ModuleInfo,
sys_info: SysInfo,
table: BindingTable,
metadata: Arc<BindingsMetadata>,
/// Multi-line ranges and ignore-all directives, computed from the AST
Expand Down Expand Up @@ -351,6 +352,7 @@ impl Bindings {
let module_info = Module::new(module_name, module_path, contents);
Self(Arc::new(BindingsInner {
module_info,
sys_info: SysInfo::default(),
table: Default::default(),
metadata: Arc::new(BindingsMetadata::new()),
module_ranges: Arc::new(ModuleRanges {
Expand Down Expand Up @@ -382,6 +384,10 @@ impl Bindings {
&self.0.module_info
}

pub fn sys_info(&self) -> &SysInfo {
&self.0.sys_info
}

pub fn metadata(&self) -> &Arc<BindingsMetadata> {
&self.0.metadata
}
Expand Down Expand Up @@ -736,6 +742,7 @@ impl Bindings {
let module_deletes = scope_trace.module_deletes().clone();
Self(Arc::new(BindingsInner {
module_info,
sys_info: builder.sys_info,
table: builder.table,
metadata: Arc::new(builder.metadata),
module_ranges,
Expand Down
53 changes: 41 additions & 12 deletions pyrefly/lib/binding/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,20 +787,49 @@ impl<'a> BindingsBuilder<'a> {
// Process the test before forking so walrus-defined names are
// in the base flow and visible to both branches.
self.ensure_expr(&mut x.test, &mut Usage::non_pinning_value_from(usage));
let static_test = self.sys_info.evaluate_bool_with_sys_info(&x.test);
let narrow_ops = NarrowOps::from_expr(self, Some(&x.test));
self.start_fork_and_branch(x.range);
self.bind_narrow_ops(&narrow_ops, NarrowUseLocation::Span(x.body.range()), usage);
self.ensure_expr(&mut x.body, usage);
// Negate the narrow ops for the `orelse`, then merge the Flows.
// TODO(stroxler): We eventually want to drop all narrows but merge values.
self.next_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::Span(x.range),
usage,
);
self.ensure_expr(&mut x.orelse, usage);
self.finish_branch();
match static_test {
Some(true) => {
self.bind_narrow_ops(
&narrow_ops,
NarrowUseLocation::Span(x.body.range()),
usage,
);
self.ensure_expr(&mut x.body, usage);
self.finish_branch();
}
Some(false) => {
self.abandon_branch();
self.start_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::Span(x.range),
usage,
);
self.ensure_expr(&mut x.orelse, usage);
self.finish_branch();
}
None => {
self.bind_narrow_ops(
&narrow_ops,
NarrowUseLocation::Span(x.body.range()),
usage,
);
self.ensure_expr(&mut x.body, usage);
// Negate the narrow ops for the `orelse`, then merge the Flows.
// TODO(stroxler): We eventually want to drop all narrows but merge values.
self.next_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::Span(x.range),
usage,
);
self.ensure_expr(&mut x.orelse, usage);
self.finish_branch();
}
}
self.finish_exhaustive_fork();
}
Expr::BoolOp(ExprBoolOp {
Expand Down
29 changes: 29 additions & 0 deletions pyrefly/lib/test/sys_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,35 @@ assert_type(Y(), int)
"#,
);

testcase!(
test_platform_ternary_linux,
TestEnv::new_with_platform(PythonPlatform::linux()),
r#"
import subprocess
import sys

value: int = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
inverted: int = 0 if sys.platform != "win32" else subprocess.CREATE_NO_WINDOW

def condition() -> bool:
return False

subprocess.CREATE_NO_WINDOW if condition() else 0 # E: No attribute `CREATE_NO_WINDOW` in module `subprocess`
"#,
);

testcase!(
test_platform_ternary_windows,
TestEnv::new_with_platform(PythonPlatform::windows()),
r#"
import subprocess
import sys

value: int = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else "not windows"
inverted: int = "not windows" if sys.platform != "win32" else subprocess.CREATE_NO_WINDOW
"#,
);

testcase!(
test_platform_membership,
r#"
Expand Down
Loading