Skip to content

Commit 7f2ee68

Browse files
martindemellometa-codesync[bot]
authored andcommitted
Do not overwrite a concrete type with None in the bindings table
Summary: The bindings table is flow-insensitive and last-write-wins - in particular, the common fallback pattern ``` try: x = factory() except: x = None ``` and similar patterns with `if` cascades, see the `None` case last and therefore end up writing it into the bindings table as the final binding for `x`. Not allowing a `None` binding to override a concrete binding fixes a lot of spurious `UnknownMethod` effects, and does not have correctness issues since a method call on `None` would crash at runtime anyway. Reviewed By: brittanyrey Differential Revision: D108084053 fbshipit-source-id: 9d2d6e16652d6ebd2e1cdb52aecc784435e61bed
1 parent 7507065 commit 7f2ee68

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

src/bindings.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ impl Value {
5656
matches!(self, Self::Unknown)
5757
}
5858

59+
pub fn is_none_type(&self) -> bool {
60+
matches!(self, Self::Instance(m) if m.as_str() == "builtins.NoneType")
61+
}
62+
5963
pub fn as_module_name(&self) -> Option<&ModuleName> {
6064
match self {
6165
Self::Module(module)
@@ -431,6 +435,20 @@ impl<'a, 'b> BindingsTableBuilder<'a, 'b> {
431435
fn add_binding(&mut self, name: Name, value: Value) {
432436
let scope = self.cursor.scope();
433437
let bindings = self.bindings.entry(scope).or_default();
438+
// A variable assigned both a concrete type and `None` (e.g. the common
439+
// `x = factory()` / `except: x = None` fallback) keeps its non-`None`
440+
// type for method resolution: a method call is only meaningful on the
441+
// non-`None` member, and calling a method on `None` crashes regardless of
442+
// lazy imports, so it is not a lazy-import concern. Bindings are
443+
// flow-insensitive, so without this a later `= None` would clobber the
444+
// concrete type and make every method call on the variable unresolvable.
445+
if value.is_none_type()
446+
&& let Some(existing) = bindings.get(&name)
447+
&& !existing.is_unknown()
448+
&& !existing.is_none_type()
449+
{
450+
return;
451+
}
434452
bindings.insert(name, value);
435453
}
436454

@@ -851,6 +869,54 @@ c = C()
851869
);
852870
}
853871

872+
#[test]
873+
fn test_none_does_not_clobber_concrete_type() {
874+
// The common `x = Factory()` / `except: x = None` fallback pattern assigns
875+
// `x` both a concrete type and `None`. For method resolution we want the
876+
// non-`None` type: a method call is only meaningful on the non-`None`
877+
// member, and calling a method on `None` crashes regardless of lazy
878+
// imports, so it is not a lazy-import concern. The `None` assignment must
879+
// therefore not clobber the concrete type.
880+
let test = r#"
881+
class A:
882+
pass
883+
884+
x = A()
885+
x = None
886+
"#;
887+
let modules = vec![("test", test)];
888+
let bt = make_bindings("test", &modules);
889+
test_instances(&bt, vec![("test", "x", "test.A")]);
890+
}
891+
892+
#[test]
893+
fn test_concrete_type_overwrites_none() {
894+
// A concrete assignment following a `None` assignment still resolves to
895+
// the concrete type (the reverse order of the fallback pattern).
896+
let test = r#"
897+
class A:
898+
pass
899+
900+
x = None
901+
x = A()
902+
"#;
903+
let modules = vec![("test", test)];
904+
let bt = make_bindings("test", &modules);
905+
test_instances(&bt, vec![("test", "x", "test.A")]);
906+
}
907+
908+
#[test]
909+
fn test_none_only_binding_stays_none() {
910+
// A variable only ever assigned `None` keeps its `NoneType` binding; the
911+
// non-`None` preference must not invent a type that was never assigned.
912+
let test = r#"
913+
x = None
914+
"#;
915+
let modules = vec![("test", test)];
916+
let bt = make_bindings("test", &modules);
917+
test_instances(&bt, vec![("test", "x", "builtins.NoneType")]);
918+
}
919+
854920
#[test]
855921
fn test_imported_constructor() {
856922
let foo = r#"

tests/calls.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@ def f(x):
190190
check_all(vec![("m1", code1), ("m2", code2)]);
191191
}
192192

193+
#[test]
194+
fn test_method_call_resolves_with_none_fallback() {
195+
// We should not infer ctx: None and raise an unknown-method error.
196+
let code = r#"
197+
class Ctx:
198+
def load_verify_locations(self):
199+
pass
200+
201+
try:
202+
ctx = Ctx()
203+
except ImportError:
204+
ctx = None
205+
206+
ctx.load_verify_locations()
207+
"#;
208+
check(code);
209+
}
210+
193211
#[test]
194212
fn test_dynamic_imports_dunder_import() {
195213
let code = r#"

0 commit comments

Comments
 (0)