Skip to content

Commit 600c8f1

Browse files
fangyi-zhoufacebook-github-bot
authored andcommitted
Raise error on variable use before write (#666)
Summary: Closes #15, #248 This change adds additional checks when building bindings, so that variables that are used before initialised are correctly flagged as errors. Currently, we error on uninitialised variables by checking their flow style. If the flow style is uninitialised or partially uninitialised, an error is generated. When no flow style information is obtainable (which is the case for use before write), a default "other" flow style value is returned. In this diff, we perform additional checks before returning a default "other" value. By looking up the scopes for static information for variables, we can correctly detect if a variable is declared in an upper scope or in the current scope, and return "uninitialised" flow style when the variable is used before write. When doing so, we need to consider the special case for type usages, so that name resolutions in types do not fail. Help needed: There's a failing test that I need some help: `test::flow::test_assert_not_in_flow` ``` testcase!( test_assert_not_in_flow, r#" from typing import assert_type, Literal if 0.1: vari = "test" raise SystemExit assert_type(vari, Literal["test"]) <-- Shall we raise an error here? "#, ); ``` Pull Request resolved: #666 Reviewed By: yangdanny97 Differential Revision: D78496802 Pulled By: stroxler fbshipit-source-id: 5c13e661f90b6b2c701e500f4aec9745b8a4a1c2
1 parent 329113c commit 600c8f1

File tree

9 files changed

+159
-28
lines changed

9 files changed

+159
-28
lines changed

conformance/third_party/conformance.exp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,16 @@
861861
"stop_column": 79,
862862
"stop_line": 18
863863
},
864+
{
865+
"code": -2,
866+
"column": 51,
867+
"concise_description": "`GoodAlias4` is uninitialized",
868+
"description": "`GoodAlias4` is uninitialized",
869+
"line": 19,
870+
"name": "unbound-name",
871+
"stop_column": 61,
872+
"stop_line": 19
873+
},
864874
{
865875
"code": -2,
866876
"column": 79,
@@ -871,6 +881,16 @@
871881
"stop_column": 83,
872882
"stop_line": 19
873883
},
884+
{
885+
"code": -2,
886+
"column": 40,
887+
"concise_description": "`GoodAlias5` is uninitialized",
888+
"description": "`GoodAlias5` is uninitialized",
889+
"line": 22,
890+
"name": "unbound-name",
891+
"stop_column": 50,
892+
"stop_line": 22
893+
},
874894
{
875895
"code": -2,
876896
"column": 17,
@@ -981,6 +1001,26 @@
9811001
"stop_column": 65,
9821002
"stop_line": 45
9831003
},
1004+
{
1005+
"code": -2,
1006+
"column": 40,
1007+
"concise_description": "`BadAlias4` is uninitialized",
1008+
"description": "`BadAlias4` is uninitialized",
1009+
"line": 46,
1010+
"name": "unbound-name",
1011+
"stop_column": 49,
1012+
"stop_line": 46
1013+
},
1014+
{
1015+
"code": -2,
1016+
"column": 44,
1017+
"concise_description": "`BadAlias5` is uninitialized",
1018+
"description": "`BadAlias5` is uninitialized",
1019+
"line": 47,
1020+
"name": "unbound-name",
1021+
"stop_column": 53,
1022+
"stop_line": 47
1023+
},
9841024
{
9851025
"code": -2,
9861026
"column": 72,
@@ -990,6 +1030,16 @@
9901030
"name": "bad-argument-type",
9911031
"stop_column": 76,
9921032
"stop_line": 47
1033+
},
1034+
{
1035+
"code": -2,
1036+
"column": 40,
1037+
"concise_description": "`BadAlias7` is uninitialized",
1038+
"description": "`BadAlias7` is uninitialized",
1039+
"line": 48,
1040+
"name": "unbound-name",
1041+
"stop_column": 49,
1042+
"stop_line": 48
9931043
}
9941044
],
9951045
"aliases_variance.py": [],
@@ -6002,6 +6052,26 @@
60026052
"stop_column": 27,
60036053
"stop_line": 18
60046054
},
6055+
{
6056+
"code": -2,
6057+
"column": 7,
6058+
"concise_description": "`T` is uninitialized",
6059+
"description": "`T` is uninitialized",
6060+
"line": 35,
6061+
"name": "unbound-name",
6062+
"stop_column": 8,
6063+
"stop_line": 35
6064+
},
6065+
{
6066+
"code": -2,
6067+
"column": 17,
6068+
"concise_description": "`T` is uninitialized",
6069+
"description": "`T` is uninitialized",
6070+
"line": 44,
6071+
"name": "unbound-name",
6072+
"stop_column": 18,
6073+
"stop_line": 44
6074+
},
60056075
{
60066076
"code": -2,
60076077
"column": 17,

conformance/third_party/conformance.result

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@
4141
],
4242
"aliases_typealiastype.py": [
4343
"Line 44: Expected 1 errors",
44-
"Line 46: Expected 1 errors",
45-
"Line 48: Expected 1 errors",
4644
"Line 52: Expected 1 errors",
4745
"Line 53: Expected 1 errors",
4846
"Line 54: Expected 1 errors",
@@ -58,7 +56,8 @@
5856
"Line 64: Expected 1 errors",
5957
"Line 17: Unexpected errors ['Argument `tuple[type[TypeVar[T]]]` is not assignable to parameter `type_params` with type `tuple[ParamSpec | TypeVar | TypeVarTuple, ...]` in function `typing.TypeAliasType.__new__`']",
6058
"Line 18: Unexpected errors ['Argument `tuple[type[TypeVar[S]], type[TypeVar[T]]]` is not assignable to parameter `type_params` with type `tuple[ParamSpec | TypeVar | TypeVarTuple, ...]` in function `typing.TypeAliasType.__new__`']",
61-
"Line 19: Unexpected errors ['Argument `tuple[type[TypeVar[T]]]` is not assignable to parameter `type_params` with type `tuple[ParamSpec | TypeVar | TypeVarTuple, ...]` in function `typing.TypeAliasType.__new__`']",
59+
"Line 19: Unexpected errors ['`GoodAlias4` is uninitialized', 'Argument `tuple[type[TypeVar[T]]]` is not assignable to parameter `type_params` with type `tuple[ParamSpec | TypeVar | TypeVarTuple, ...]` in function `typing.TypeAliasType.__new__`']",
60+
"Line 22: Unexpected errors ['`GoodAlias5` is uninitialized']",
6261
"Line 23: Unexpected errors ['Argument `tuple[type[TypeVar[S]], type[TypeVar[TStr]], type[ParamSpec[P]], type[TypeVarTuple[Ts]]]` is not assignable to parameter `type_params` with type `tuple[ParamSpec | TypeVar | TypeVarTuple, ...]` in function `typing.TypeAliasType.__new__`']",
6362
"Line 35: Unexpected errors ['Expected a type form, got instance of `GenericAlias`']",
6463
"Line 36: Unexpected errors ['Expected a type form, got instance of `GenericAlias`']",
@@ -263,7 +262,6 @@
263262
"generics_syntax_declarations.py": [],
264263
"generics_syntax_infer_variance.py": [],
265264
"generics_syntax_scoping.py": [
266-
"Line 35: Expected 1 errors",
267265
"Line 92: Expected 1 errors",
268266
"Line 95: Expected 1 errors",
269267
"Line 98: Expected 1 errors",

conformance/third_party/results.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"pass": 70,
44
"fail": 66,
55
"pass_rate": 0.51,
6-
"differences": 293,
6+
"differences": 291,
77
"passing": [
88
"aliases_explicit.py",
99
"aliases_newtype.py",
@@ -80,7 +80,7 @@
8080
"aliases_implicit.py": 6,
8181
"aliases_recursive.py": 17,
8282
"aliases_type_statement.py": 8,
83-
"aliases_typealiastype.py": 25,
83+
"aliases_typealiastype.py": 24,
8484
"aliases_variance.py": 4,
8585
"annotations_forward_refs.py": 7,
8686
"annotations_generators.py": 3,
@@ -109,7 +109,7 @@
109109
"generics_self_basic.py": 3,
110110
"generics_self_protocols.py": 1,
111111
"generics_self_usage.py": 12,
112-
"generics_syntax_scoping.py": 7,
112+
"generics_syntax_scoping.py": 6,
113113
"generics_typevartuple_basic.py": 1,
114114
"generics_typevartuple_concat.py": 1,
115115
"generics_typevartuple_specialization.py": 4,

pyrefly/lib/binding/expr.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ impl<'a> BindingsBuilder<'a> {
217217
&mut self,
218218
name: &Identifier,
219219
value: Result<Binding, LookupError>,
220+
used_in_static_type: bool,
220221
) -> Idx<Key> {
221222
let key = Key::BoundName(ShortIdentifier::new(name));
222223
if name.is_empty() {
@@ -236,7 +237,7 @@ impl<'a> BindingsBuilder<'a> {
236237
// Don't check flow for global/nonlocal lookups
237238
if let Some(error_message) = self
238239
.scopes
239-
.get_flow_style(&name.id)
240+
.get_flow_style(&name.id, used_in_static_type)
240241
.uninitialized_error_message(name)
241242
{
242243
self.error(
@@ -636,7 +637,11 @@ impl<'a> BindingsBuilder<'a> {
636637
let binding = self
637638
.lookup_name_usage(Hashed::new(&name.id), usage)
638639
.map(Binding::Forward);
639-
self.ensure_name(&name, binding);
640+
self.ensure_name(
641+
&name,
642+
binding,
643+
matches!(usage, Usage::StaticTypeInformation),
644+
);
640645
}
641646
Expr::Yield(x) => {
642647
self.record_yield(x.clone());
@@ -673,7 +678,7 @@ impl<'a> BindingsBuilder<'a> {
673678
.lookup_name(Hashed::new(&name.id), LookupKind::Regular)
674679
.map(Binding::Forward),
675680
};
676-
self.ensure_name(&name, binding);
681+
self.ensure_name(&name, binding, true);
677682
}
678683
Expr::Subscript(ExprSubscript { value, .. })
679684
if self.as_special_export(value) == Some(SpecialExport::Literal) =>

pyrefly/lib/binding/scope.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,10 +806,47 @@ impl Scopes {
806806
None
807807
}
808808

809-
pub fn get_flow_style(&self, name: &Name) -> &FlowStyle {
809+
fn get_static_info(&self, name: &Name, should_skip_current_scope: bool) -> Option<&StaticInfo> {
810+
let name = Hashed::new(name);
811+
let mut iter = self.iter_rev();
812+
if should_skip_current_scope {
813+
iter.next();
814+
}
815+
for scope in iter {
816+
if let Some(info) = scope.stat.0.get_hashed(name) {
817+
return Some(info);
818+
}
819+
}
820+
None
821+
}
822+
823+
/// Get the flow style for `name`, depending on whether `name` is used in a
824+
/// static type.
825+
///
826+
/// If we can find a flow info for `name`, return its style. Otherwise, we
827+
/// check the static type information to see if we have a uninitialized
828+
/// binding, in which case, `FlowStyle::Uninitialized` is returned.
829+
/// Otherwise we return `FlowStyle::Other` to indicate no information
830+
/// available.
831+
pub fn get_flow_style(&self, name: &Name, used_in_static_type: bool) -> &FlowStyle {
810832
match self.get_flow_info(name) {
811833
Some(flow) => &flow.style,
812-
None => &FlowStyle::Other,
834+
None => {
835+
// If the name is used for static type information, we can look
836+
// at the current scope.
837+
// Otherwise, we should skip the current scope, because it may
838+
// permit a name to be used before it is defined.
839+
if self.get_static_info(name, !used_in_static_type).is_some() {
840+
// If we have a static binding, then we are in a scope where
841+
// the name is defined, so we can return Other.
842+
&FlowStyle::Other
843+
} else {
844+
// If we don't have a static binding, then we are in a scope
845+
// where the name is not defined, so we return
846+
// Uninitialized.
847+
&FlowStyle::Uninitialized
848+
}
849+
}
813850
}
814851
}
815852

pyrefly/lib/binding/stmt.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,16 @@ impl<'a> BindingsBuilder<'a> {
153153
})
154154
}
155155

156-
pub fn ensure_mutable_name(&mut self, x: &ExprName) -> Idx<Key> {
156+
pub fn ensure_mutable_name(&mut self, x: &ExprName, usage: &mut Usage) -> Idx<Key> {
157157
let name = Ast::expr_name_identifier(x.clone());
158158
let binding = self
159159
.lookup_name(Hashed::new(&name.id), LookupKind::Mutable)
160160
.map(Binding::Forward);
161-
self.ensure_name(&name, binding)
161+
self.ensure_name(
162+
&name,
163+
binding,
164+
matches!(usage, Usage::StaticTypeInformation),
165+
)
162166
}
163167

164168
fn define_nonlocal_name(&mut self, name: &Identifier) {
@@ -271,7 +275,7 @@ impl<'a> BindingsBuilder<'a> {
271275
for target in &mut x.targets {
272276
let mut delete_link = self.declare_current_idx(Key::UsageLink(target.range()));
273277
if let Expr::Name(name) = target {
274-
let idx = self.ensure_mutable_name(name);
278+
let idx = self.ensure_mutable_name(name, delete_link.usage());
275279
self.scopes.upsert_flow_info(
276280
Hashed::new(&name.id),
277281
idx,
@@ -527,7 +531,7 @@ impl<'a> BindingsBuilder<'a> {
527531
.declare_current_idx(Key::Definition(ShortIdentifier::expr_name(name)));
528532
// Ensure the target name, which must already be in scope (it is part of the implicit dunder method call
529533
// used in augmented assignment).
530-
self.ensure_mutable_name(name);
534+
self.ensure_mutable_name(name, assigned.usage());
531535
self.ensure_expr(&mut x.value, assigned.usage());
532536
// TODO(stroxler): Should we really be using `bind_key` here? This will update the
533537
// flow info to define the name, even if it was not previously defined.

pyrefly/lib/test/assign.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,17 @@ x3: B
625625
);
626626

627627
testcase!(
628-
bug = "False negative",
628+
test_use_before_write,
629+
r#"
630+
y # E: `y` is uninitialized
631+
y = 42
632+
"#,
633+
);
634+
635+
testcase!(
629636
test_read_before_write,
630637
r#"
631-
x = y # this should be an error
638+
x = y # E: `y` is uninitialized
632639
y = 42
633640
"#,
634641
);
@@ -757,7 +764,7 @@ def f(x):
757764
758765
def f(x):
759766
x = 3
760-
x = "None"
767+
x = "None"
761768
"#,
762769
);
763770

pyrefly/lib/test/flow.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ def foo() -> list[int]:
11771177
results: list[int] = [1, 2, 3]
11781178
for i, x in enumerate(results):
11791179
results[i] = x * 10
1180-
return results
1180+
return results
11811181
"#,
11821182
);
11831183

@@ -1201,7 +1201,7 @@ bar: str = "bar"
12011201
12021202
def func():
12031203
foo: str | None = None
1204-
1204+
12051205
for x in []:
12061206
for y in []:
12071207
pass
@@ -1226,7 +1226,7 @@ from typing import assert_type, Literal
12261226
if 0.1:
12271227
vari = "test"
12281228
raise SystemExit
1229-
assert_type(vari, Literal["test"])
1229+
assert_type(vari, Literal["test"]) # E: `vari` is uninitialized
12301230
"#,
12311231
);
12321232

pyrefly/lib/test/scope.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def f():
7575
x += "a" # E: `x` is not mutable from the current scope
7676
def g():
7777
global x
78-
x += "a"
78+
x += "a"
7979
def h0():
8080
global x
8181
def h1():
@@ -173,12 +173,12 @@ def f():
173173
global c1
174174
global c2
175175
# Should be permitted, the resulting operation is in-place
176-
c0 += C()
176+
c0 += C()
177177
# Should be permitted, the resulting operation returns a new C which is okay
178-
c1 -= C()
178+
c1 -= C()
179179
# Should *not* be permitted, this changes the type of the global in a way
180180
# that is incompatible with static analysis of the global scope
181-
c2 *= C()
181+
c2 *= C()
182182
f()
183183
# This shows what would go wrong if we allow the aug assign on `c2`
184184
assert_type(c2, C)
@@ -234,7 +234,7 @@ def outer():
234234
x += "a" # E: `x` is not mutable from the current scope
235235
def g():
236236
nonlocal x
237-
x += "a"
237+
x += "a"
238238
def h0():
239239
nonlocal x
240240
def h1():
@@ -258,7 +258,7 @@ def outer():
258258
# A minor variation on f(), relevant to specific implementation bugs in our scope analysis
259259
def g():
260260
nonlocal x
261-
del x
261+
del x
262262
f()
263263
f() # This will crash at runtime!
264264
"#,
@@ -437,3 +437,13 @@ def f(arg: int) -> None:
437437
z # E: Could not find name `z`
438438
"#,
439439
);
440+
441+
testcase!(
442+
test_forward_reference_ok,
443+
r#"
444+
def foo():
445+
x = y
446+
447+
y = 42
448+
"#,
449+
);

0 commit comments

Comments
 (0)