mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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:
@@ -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;
|
||||
}
|
||||
+176
@@ -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: [],
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user