diff --git a/compiler/forget/crates/forget_semantic_analysis/src/analyzer.rs b/compiler/forget/crates/forget_semantic_analysis/src/analyzer.rs index 33ae17bedd..03af4146ab 100644 --- a/compiler/forget/crates/forget_semantic_analysis/src/analyzer.rs +++ b/compiler/forget/crates/forget_semantic_analysis/src/analyzer.rs @@ -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, + // 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(), }); } } diff --git a/compiler/forget/crates/forget_semantic_analysis/src/scope_manager.rs b/compiler/forget/crates/forget_semantic_analysis/src/scope_manager.rs index 6a69a6814b..d55e0c3747 100644 --- a/compiler/forget/crates/forget_semantic_analysis/src/scope_manager.rs +++ b/compiler/forget/crates/forget_semantic_analysis/src/scope_manager.rs @@ -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)] diff --git a/compiler/forget/crates/forget_semantic_analysis/tests/fixtures/tdz.js b/compiler/forget/crates/forget_semantic_analysis/tests/fixtures/tdz.js new file mode 100644 index 0000000000..71e8e8ba33 --- /dev/null +++ b/compiler/forget/crates/forget_semantic_analysis/tests/fixtures/tdz.js @@ -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; +} diff --git a/compiler/forget/crates/forget_semantic_analysis/tests/snapshots/analysis_test__fixtures@tdz.js.snap b/compiler/forget/crates/forget_semantic_analysis/tests/snapshots/analysis_test__fixtures@tdz.js.snap new file mode 100644 index 0000000000..cd591f3797 --- /dev/null +++ b/compiler/forget/crates/forget_semantic_analysis/tests/snapshots/analysis_test__fixtures@tdz.js.snap @@ -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: [], + }, +) +