@samitouri / QOS-React-1 / commits / 796080f4d1

[rust][sema] Statically detect some TDZ violations

The [temporal dead zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz), often abbreviated TDZ, is the period between the start of its declaring block and the line that contains the let/const/class declaration. Not all instances of TDZ can be detected statically, because whether or not a TDZ error will occur at runtime is a property of which control flow path is taken and whether some other code has initialized the value. However, a subset of cases can be detected, and that's what we implement here. When we encounter a variable reference we record the next declaration id at that point in time. Then when resolving references after visiting the program, we can check: did the reference end up referring to a let/const/class binding whose id is equal or greater to that "next declaration"? If so, it means the reference refers to a variable that is provably declared later and is a known TDZ violation. The catch is that when resolving references, we reset the "next declaration" limit value when we bubble up out of a function scope. That's because references to let/const within a function may occur after the declaration, and we can't statically validate them.

Joe Savona committed Aug 15, 2023 at 17:20 UTC 796080f4d1797ba2819b736e1e2ce2cfb6512d98
4 files changed +249 -6
compiler/forget/crates/forget_semantic_analysis/src/analyzer.rs
+14 -6
@@ -7,8 +7,8 @@ use forget_estree::{
7 };
8
9 use crate::{
10 - AstNode, DeclarationKind, Label, LabelId, LabelKind, ReferenceKind, ScopeId, ScopeKind,
11 - ScopeManager,
10 + AstNode, DeclarationId, DeclarationKind, Label, LabelId, LabelKind, ReferenceKind, ScopeId,
11 + ScopeKind, ScopeManager,
12 };
13
14 pub fn analyze(ast: &Program) -> ScopeManager {
@@ -31,6 +31,11 @@ pub struct UnresolvedReference {
31 pub name: String,
32 pub kind: ReferenceKind,
33 pub range: Option<SourceRange>,
34 + // The next declaration id at the time the reference was created
35 + // this is used to detect a subset of TDZ violations, where a
36 + // reference is trivially known to refer to a let/const declaration
37 + // that cannot have been initialized yet.
38 + pub next_declaration: DeclarationId,
39 }
40
41 impl Analyzer {
@@ -48,10 +53,11 @@ impl Analyzer {
53
54 fn complete(mut self) -> ScopeManager {
55 for reference in self.unresolved {
51 - if let Some(declaration) = self
52 - .manager
53 - .lookup_declaration(reference.scope, &reference.name)
54 - {
56 + if let Some(declaration) = self.manager.lookup_reference(
57 + reference.scope,
58 + &reference.name,
59 + reference.next_declaration,
60 + ) {
61 let id =
62 self.manager
63 .add_reference(reference.scope, reference.kind, declaration.id);
@@ -188,6 +194,7 @@ impl Analyzer {
194 name: name.to_string(),
195 kind,
196 range,
197 + next_declaration: self.manager.next_declaration_id(),
198 });
199 }
200
@@ -224,6 +231,7 @@ impl Analyzer {
231 name: ast.name.to_string(),
232 kind: ReferenceKind::Write,
233 range: ast.range,
234 + next_declaration: self.manager.next_declaration_id(),
235 });
236 }
237 }
compiler/forget/crates/forget_semantic_analysis/src/scope_manager.rs
+45
@@ -188,6 +188,47 @@ impl ScopeManager {
188 }
189 }
190
191 + pub fn lookup_reference(
192 + &self,
193 + scope: ScopeId,
194 + name: &str,
195 + next_declaration: DeclarationId,
196 + ) -> Option<&Declaration> {
197 + let mut current = &self.scopes[scope.0];
198 + let mut tdz_limit = Some(next_declaration);
199 + loop {
200 + if let Some(id) = current.declarations.get(name) {
201 + let declaration = self.declaration(*id);
202 +
203 + // Basic static check for TDZ violations. If there is still a
204 + // tdz limit (see below where we reset when leaving function scopes)
205 + // then we check if the declaration is let/const and came after the
206 + // reference. If so it's a TDZ violation
207 + if let Some(tdz_limit) = tdz_limit {
208 + if (declaration.kind == DeclarationKind::Let
209 + || declaration.kind == DeclarationKind::Const)
210 + && id.0 >= tdz_limit.0
211 + {
212 + return None;
213 + }
214 + }
215 + return Some(&self.declarations[id.0]);
216 + }
217 + if let Some(parent) = current.parent {
218 + // When leaving a function scope, clear the tdz limit.
219 + // This means we won't report TDZ violations for references
220 + // inside functions to hoisted let/const variables defined
221 + // outside the function
222 + if current.kind == ScopeKind::Function {
223 + tdz_limit = None;
224 + }
225 + current = &self.scopes[parent.0];
226 + } else {
227 + return None;
228 + }
229 + }
230 + }
231 +
232 pub(crate) fn root_id(&self) -> ScopeId {
233 self.root
234 }
@@ -295,6 +336,10 @@ impl ScopeManager {
336 self.scopes[scope.0].references.push(id);
337 id
338 }
339 +
340 + pub(crate) fn next_declaration_id(&self) -> DeclarationId {
341 + DeclarationId(self.declarations.len())
342 + }
343 }
344
345 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
compiler/forget/crates/forget_semantic_analysis/tests/fixtures/tdz.js new
+14
@@ -0,0 +1,14 @@
1 +function Component() {
2 + a; // invalid
3 + if (true) {
4 + a; // invalid
5 + }
6 + for (;;) {
7 + a; // invalid
8 + }
9 + function foo() {
10 + a; // will be a runtime tdz error but we don't detect that statically
11 + }
12 + foo(); // above is a runtime tdz error bc of this call
13 + let a;
14 +}
compiler/forget/crates/forget_semantic_analysis/tests/snapshots/analysis_test__fixtures@tdz.js.snap new
+176
@@ -0,0 +1,176 @@
1 +---
2 +source: crates/forget_semantic_analysis/tests/analysis_test.rs
3 +expression: "format!(\"Input:\\n{input}\\n\\nAnalysis:\\n{output}\")"
4 +input_file: crates/forget_semantic_analysis/tests/fixtures/tdz.js
5 +---
6 +Input:
7 +function Component() {
8 + a; // invalid
9 + if (true) {
10 + a; // invalid
11 + }
12 + for (;;) {
13 + a; // invalid
14 + }
15 + function foo() {
16 + a; // will be a runtime tdz error but we don't detect that statically
17 + }
18 + foo(); // above is a runtime tdz error bc of this call
19 + let a;
20 +}
21 +
22 +
23 +Analysis:
24 +Scope {
25 + id: ScopeId(
26 + 0,
27 + ),
28 + kind: Module,
29 + declarations: {
30 + "Component": Declaration {
31 + id: DeclarationId(
32 + 0,
33 + ),
34 + kind: FunctionDeclaration,
35 + scope: ScopeId(
36 + 0,
37 + ),
38 + },
39 + },
40 + references: [],
41 + children: [
42 + Scope {
43 + id: ScopeId(
44 + 1,
45 + ),
46 + kind: Function,
47 + declarations: {
48 + "foo": Declaration {
49 + id: DeclarationId(
50 + 1,
51 + ),
52 + kind: FunctionDeclaration,
53 + scope: ScopeId(
54 + 1,
55 + ),
56 + },
57 + "a": Declaration {
58 + id: DeclarationId(
59 + 2,
60 + ),
61 + kind: Let,
62 + scope: ScopeId(
63 + 1,
64 + ),
65 + },
66 + },
67 + references: [
68 + Reference {
69 + id: ReferenceId(
70 + 1,
71 + ),
72 + kind: Read,
73 + declaration: DeclarationId(
74 + 1,
75 + ),
76 + declaration (name): "foo",
77 + scope: ScopeId(
78 + 1,
79 + ),
80 + },
81 + ],
82 + children: [
83 + Scope {
84 + id: ScopeId(
85 + 2,
86 + ),
87 + kind: Block,
88 + declarations: {},
89 + references: [],
90 + children: [],
91 + },
92 + Scope {
93 + id: ScopeId(
94 + 3,
95 + ),
96 + kind: Block,
97 + declarations: {},
98 + references: [],
99 + children: [],
100 + },
101 + Scope {
102 + id: ScopeId(
103 + 4,
104 + ),
105 + kind: Function,
106 + declarations: {},
107 + references: [
108 + Reference {
109 + id: ReferenceId(
110 + 0,
111 + ),
112 + kind: Read,
113 + declaration: DeclarationId(
114 + 2,
115 + ),
116 + declaration (name): "a",
117 + scope: ScopeId(
118 + 4,
119 + ),
120 + },
121 + ],
122 + children: [],
123 + },
124 + ],
125 + },
126 + ],
127 +}
128 +Diagnostic(
129 + DiagnosticData {
130 + message: "Undefined variable",
131 + span: Some(
132 + SourceSpan {
133 + offset: SourceOffset(
134 + 25,
135 + ),
136 + length: 1,
137 + },
138 + ),
139 + related_information: [],
140 + severity: InvalidSyntax,
141 + data: [],
142 + },
143 +)
144 +Diagnostic(
145 + DiagnosticData {
146 + message: "Undefined variable",
147 + span: Some(
148 + SourceSpan {
149 + offset: SourceOffset(
150 + 57,
151 + ),
152 + length: 1,
153 + },
154 + ),
155 + related_information: [],
156 + severity: InvalidSyntax,
157 + data: [],
158 + },
159 +)
160 +Diagnostic(
161 + DiagnosticData {
162 + message: "Undefined variable",
163 + span: Some(
164 + SourceSpan {
165 + offset: SourceOffset(
166 + 92,
167 + ),
168 + length: 1,
169 + },
170 + ),
171 + related_information: [],
172 + severity: InvalidSyntax,
173 + data: [],
174 + },
175 +)
176 +