[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.
This commit is contained in:
Joe Savona
2023-08-15 17:20:34 -07:00
parent 2edf66ecdf
commit 796080f4d1
4 changed files with 249 additions and 6 deletions
@@ -7,8 +7,8 @@ use forget_estree::{
};
use crate::{
AstNode, DeclarationKind, Label, LabelId, LabelKind, ReferenceKind, ScopeId, ScopeKind,
ScopeManager,
AstNode, DeclarationId, DeclarationKind, Label, LabelId, LabelKind, ReferenceKind, ScopeId,
ScopeKind, ScopeManager,
};
pub fn analyze(ast: &Program) -> ScopeManager {
@@ -31,6 +31,11 @@ pub struct UnresolvedReference {
pub name: String,
pub kind: ReferenceKind,
pub range: Option<SourceRange>,
// The next declaration id at the time the reference was created
// this is used to detect a subset of TDZ violations, where a
// reference is trivially known to refer to a let/const declaration
// that cannot have been initialized yet.
pub next_declaration: DeclarationId,
}
impl Analyzer {
@@ -48,10 +53,11 @@ impl Analyzer {
fn complete(mut self) -> ScopeManager {
for reference in self.unresolved {
if let Some(declaration) = self
.manager
.lookup_declaration(reference.scope, &reference.name)
{
if let Some(declaration) = self.manager.lookup_reference(
reference.scope,
&reference.name,
reference.next_declaration,
) {
let id =
self.manager
.add_reference(reference.scope, reference.kind, declaration.id);
@@ -188,6 +194,7 @@ impl Analyzer {
name: name.to_string(),
kind,
range,
next_declaration: self.manager.next_declaration_id(),
});
}
@@ -224,6 +231,7 @@ impl Analyzer {
name: ast.name.to_string(),
kind: ReferenceKind::Write,
range: ast.range,
next_declaration: self.manager.next_declaration_id(),
});
}
}
@@ -188,6 +188,47 @@ impl ScopeManager {
}
}
pub fn lookup_reference(
&self,
scope: ScopeId,
name: &str,
next_declaration: DeclarationId,
) -> Option<&Declaration> {
let mut current = &self.scopes[scope.0];
let mut tdz_limit = Some(next_declaration);
loop {
if let Some(id) = current.declarations.get(name) {
let declaration = self.declaration(*id);
// Basic static check for TDZ violations. If there is still a
// tdz limit (see below where we reset when leaving function scopes)
// then we check if the declaration is let/const and came after the
// reference. If so it's a TDZ violation
if let Some(tdz_limit) = tdz_limit {
if (declaration.kind == DeclarationKind::Let
|| declaration.kind == DeclarationKind::Const)
&& id.0 >= tdz_limit.0
{
return None;
}
}
return Some(&self.declarations[id.0]);
}
if let Some(parent) = current.parent {
// When leaving a function scope, clear the tdz limit.
// This means we won't report TDZ violations for references
// inside functions to hoisted let/const variables defined
// outside the function
if current.kind == ScopeKind::Function {
tdz_limit = None;
}
current = &self.scopes[parent.0];
} else {
return None;
}
}
}
pub(crate) fn root_id(&self) -> ScopeId {
self.root
}
@@ -295,6 +336,10 @@ impl ScopeManager {
self.scopes[scope.0].references.push(id);
id
}
pub(crate) fn next_declaration_id(&self) -> DeclarationId {
DeclarationId(self.declarations.len())
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
@@ -0,0 +1,14 @@
function Component() {
a; // invalid
if (true) {
a; // invalid
}
for (;;) {
a; // invalid
}
function foo() {
a; // will be a runtime tdz error but we don't detect that statically
}
foo(); // above is a runtime tdz error bc of this call
let a;
}
@@ -0,0 +1,176 @@
---
source: crates/forget_semantic_analysis/tests/analysis_test.rs
expression: "format!(\"Input:\\n{input}\\n\\nAnalysis:\\n{output}\")"
input_file: crates/forget_semantic_analysis/tests/fixtures/tdz.js
---
Input:
function Component() {
a; // invalid
if (true) {
a; // invalid
}
for (;;) {
a; // invalid
}
function foo() {
a; // will be a runtime tdz error but we don't detect that statically
}
foo(); // above is a runtime tdz error bc of this call
let a;
}
Analysis:
Scope {
id: ScopeId(
0,
),
kind: Module,
declarations: {
"Component": Declaration {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
scope: ScopeId(
0,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
1,
),
kind: Function,
declarations: {
"foo": Declaration {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
scope: ScopeId(
1,
),
},
"a": Declaration {
id: DeclarationId(
2,
),
kind: Let,
scope: ScopeId(
1,
),
},
},
references: [
Reference {
id: ReferenceId(
1,
),
kind: Read,
declaration: DeclarationId(
1,
),
declaration (name): "foo",
scope: ScopeId(
1,
),
},
],
children: [
Scope {
id: ScopeId(
2,
),
kind: Block,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
3,
),
kind: Block,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
4,
),
kind: Function,
declarations: {},
references: [
Reference {
id: ReferenceId(
0,
),
kind: Read,
declaration: DeclarationId(
2,
),
declaration (name): "a",
scope: ScopeId(
4,
),
},
],
children: [],
},
],
},
],
}
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
25,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
57,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
92,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)