diff --git a/Cargo.lock b/Cargo.lock index 828edd3ca..964a4b0fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,6 +900,7 @@ dependencies = [ name = "oxc_semantic" version = "0.0.0" dependencies = [ + "bitflags", "indextree", "oxc_ast", ] diff --git a/README.md b/README.md index 7195731f4..00aede0a8 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ This project partially copies code from the following projects: | [microsoft/TypeScript](https://github.com/microsoft/TypeScript) | [Apache 2.0](https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt) | | [rome/tools](https://github.com/rome/tools) | [MIT](https://github.com/rome/tools/blob/main/LICENSE) | | [mozilla-spidermonkey/jsparagus](https://github.com/mozilla-spidermonkey/jsparagus) | [MIT](https://github.com/mozilla-spidermonkey/jsparagus/blob/master/LICENSE-MIT) [Apache 2.0](https://github.com/mozilla-spidermonkey/jsparagus/blob/master/LICENSE-APACHE-2.0) | +| [acorn](https://github.com/acornjs/acorn) | [MIT](https://github.com/acornjs/acorn/blob/master/acorn/LICENSE) | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg?color=brightgreen diff --git a/THIRD-PARTY-LICENSE b/THIRD-PARTY-LICENSE index 1cf70ce6c..0e28801e5 100644 --- a/THIRD-PARTY-LICENSE +++ b/THIRD-PARTY-LICENSE @@ -146,3 +146,29 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +acorn + +MIT License + +Copyright (C) 2012-2022 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/crates/oxc_cli/src/lib.rs b/crates/oxc_cli/src/lib.rs index 78138f72b..0929e08ed 100644 --- a/crates/oxc_cli/src/lib.rs +++ b/crates/oxc_cli/src/lib.rs @@ -135,7 +135,7 @@ impl Cli { let program = allocator.alloc(ret.program); let trivias = Rc::new(ret.trivias); - let semantic = SemanticBuilder::new().build(program, trivias); + let semantic = SemanticBuilder::new(source_type).build(program, trivias); let result = Linter::new().run(&Rc::new(semantic), &source_text, fix); if result.is_empty() { diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index 12917626e..cf03ae802 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -63,7 +63,7 @@ impl Tester { assert!(ret.errors.is_empty(), "{:?}", &ret.errors); let program = allocator.alloc(ret.program); let trivias = Rc::new(ret.trivias); - let semantic = SemanticBuilder::new().build(program, trivias); + let semantic = SemanticBuilder::new(source_type).build(program, trivias); let semantic = Rc::new(semantic); let rule = RULES .iter() diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index 3c2e3b4ec..95101d8d3 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -13,3 +13,4 @@ version.workspace = true oxc_ast = { path = "../oxc_ast" } indextree = { workspace = true } +bitflags = { workspace = true } diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 599fa4d6b..fc54d2fbe 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -4,28 +4,33 @@ use std::rc::Rc; -use oxc_ast::{ast::Program, visit::Visit, AstKind, Trivias}; +use oxc_ast::{ast::Program, visit::Visit, AstKind, SourceType, Trivias}; use crate::{ - node::{AstNodeId, AstNodes, SemanticNode}, + node::{AstNodeId, AstNodes, NodeFlags, SemanticNode}, + scope::ScopeBuilder, Semantic, }; -#[derive(Debug)] pub struct SemanticBuilder<'a> { - nodes: AstNodes<'a>, - + // states current_node_id: AstNodeId, + current_node_flags: NodeFlags, + + // builders + nodes: AstNodes<'a>, + scope: ScopeBuilder, } impl<'a> SemanticBuilder<'a> { #[must_use] - #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn new(source_type: SourceType) -> Self { + let scope = ScopeBuilder::new(source_type); let mut nodes = AstNodes::default(); - let semantic_node = SemanticNode::new(AstKind::Root); + let semantic_node = + SemanticNode::new(AstKind::Root, scope.current_scope_id, NodeFlags::empty()); let current_node_id = nodes.new_node(semantic_node).into(); - Self { nodes, current_node_id } + Self { current_node_id, nodes, scope, current_node_flags: NodeFlags::empty() } } #[must_use] @@ -36,7 +41,8 @@ impl<'a> SemanticBuilder<'a> { } fn create_ast_node(&mut self, kind: AstKind<'a>) { - let ast_node = SemanticNode::new(kind); + let ast_node = + SemanticNode::new(kind, self.scope.current_scope_id, self.current_node_flags); let node_id = self.nodes.new_node(ast_node); self.current_node_id.append(node_id, &mut self.nodes); self.current_node_id = node_id.into(); @@ -46,14 +52,35 @@ impl<'a> SemanticBuilder<'a> { self.current_node_id = self.nodes[self.current_node_id.indextree_id()].parent().unwrap().into(); } + + fn try_enter_scope(&mut self, kind: AstKind<'a>) { + if let Some(flags) = ScopeBuilder::scope_flags_from_ast_kind(kind) { + self.scope.enter(flags); + } + } + + fn try_leave_scope(&mut self, kind: AstKind<'a>) { + if ScopeBuilder::scope_flags_from_ast_kind(kind).is_some() + || matches!(kind, AstKind::Program(_)) + { + self.scope.leave(); + } + } } impl<'a> Visit<'a> for SemanticBuilder<'a> { + // Setup all the context for the binder, + // the order is important here. fn enter_node(&mut self, kind: AstKind<'a>) { + // create new self.scope.current_scope_id + self.try_enter_scope(kind); + + // create new self.current_node_id self.create_ast_node(kind); } - fn leave_node(&mut self, _kind: AstKind<'a>) { + fn leave_node(&mut self, kind: AstKind<'a>) { self.pop_ast_node(); + self.try_leave_scope(kind); } } diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 21f7b290f..5aab40dbe 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -1,11 +1,13 @@ mod builder; mod node; +mod scope; use std::rc::Rc; pub use builder::SemanticBuilder; pub use node::{AstNode, AstNodes}; use oxc_ast::Trivias; +pub use scope::{Scope, ScopeTree}; pub struct Semantic<'a> { nodes: AstNodes<'a>, diff --git a/crates/oxc_semantic/src/node/mod.rs b/crates/oxc_semantic/src/node/mod.rs index 73edd9aeb..a2762c9b2 100644 --- a/crates/oxc_semantic/src/node/mod.rs +++ b/crates/oxc_semantic/src/node/mod.rs @@ -1,9 +1,13 @@ +#![allow(non_upper_case_globals)] // for bitflags + mod id; mod tree; -pub use id::AstNodeId; +use bitflags::bitflags; use oxc_ast::AstKind; -pub use tree::AstNodes; + +pub use self::{id::AstNodeId, tree::AstNodes}; +use crate::scope::{Scope, ScopeId}; /// Indextree node containing a semantic node pub type AstNode<'a> = indextree::Node>; @@ -13,15 +17,42 @@ pub type AstNode<'a> = indextree::Node>; pub struct SemanticNode<'a> { /// A pointer to the ast node, which resides in the `bumpalo` memory arena. kind: AstKind<'a>, + + /// Associated Scope (initialized by binding) + scope_id: ScopeId, + + flags: NodeFlags, +} + +bitflags! { + #[derive(Default)] + pub struct NodeFlags: u8 { + const Class = 1 << 0; // If Node is inside a class + } } impl<'a> SemanticNode<'a> { #[must_use] - pub const fn new(kind: AstKind<'a>) -> Self { - Self { kind } + pub const fn new(kind: AstKind<'a>, scope_id: ScopeId, flags: NodeFlags) -> Self { + Self { kind, scope_id, flags } } pub const fn kind(&self) -> AstKind<'a> { self.kind } + + pub const fn scope_id(&self) -> ScopeId { + self.scope_id + } + + #[must_use] + pub const fn strict_mode(&self, scope: &Scope) -> bool { + // All parts of a ClassDeclaration or a ClassExpression are strict mode code. + scope.strict_mode() || self.in_class() + } + + #[must_use] + pub const fn in_class(self) -> bool { + self.flags.contains(NodeFlags::Class) + } } diff --git a/crates/oxc_semantic/src/scope/builder.rs b/crates/oxc_semantic/src/scope/builder.rs new file mode 100644 index 000000000..c6ee05a97 --- /dev/null +++ b/crates/oxc_semantic/src/scope/builder.rs @@ -0,0 +1,71 @@ +use oxc_ast::{AstKind, SourceType}; + +use super::{Scope, ScopeFlags, ScopeId, ScopeTree}; + +#[derive(Debug)] +pub struct ScopeBuilder { + pub scopes: ScopeTree, + + pub current_scope_id: ScopeId, +} + +impl ScopeBuilder { + #[must_use] + pub fn new(source_type: SourceType) -> Self { + // Module code is always strict mode code. + let strict_mode = source_type.is_module(); + let scopes = ScopeTree::new(strict_mode); + let current_scope_id = scopes.root_scope_id(); + Self { scopes, current_scope_id } + } + + pub fn enter(&mut self, flags: ScopeFlags) { + // Inherit strict mode for functions + // https://tc39.es/ecma262/#sec-strict-mode-code + let mut strict_mode = self.scopes[self.scopes.root_scope_id()].strict_mode; + let parent_scope = self.current_scope(); + if !strict_mode && parent_scope.is_function() && parent_scope.strict_mode { + strict_mode = true; + } + + // inherit flags for non-function scopes + let flags = if flags.contains(ScopeFlags::Function) { + flags + } else { + flags | (parent_scope.flags & ScopeFlags::MODIFIERS) + }; + + let scope = Scope::new(flags, strict_mode); + let new_scope_id = self.scopes.new_node(scope); + self.current_scope_id.append(new_scope_id, &mut self.scopes); + self.current_scope_id = new_scope_id.into(); + } + + pub fn leave(&mut self) { + if let Some(parent_id) = self.scopes[self.current_scope_id.indextree_id()].parent() { + self.current_scope_id = parent_id.into(); + } + } + + #[must_use] + pub fn current_scope(&self) -> &Scope { + &self.scopes[self.current_scope_id] + } + + #[must_use] + pub fn scope_flags_from_ast_kind(kind: AstKind) -> Option { + match kind { + AstKind::Function(_) => Some(ScopeFlags::Function), + AstKind::ArrowExpression(_) => Some(ScopeFlags::Function | ScopeFlags::Arrow), + AstKind::StaticBlock(_) => Some(ScopeFlags::ClassStaticBlock), + AstKind::TSModuleBlock(_) => Some(ScopeFlags::TsModuleBlock), + AstKind::BlockStatement(_) + | AstKind::CatchClause(_) + | AstKind::ForStatement(_) + | AstKind::ForInStatement(_) + | AstKind::ForOfStatement(_) + | AstKind::SwitchStatement(_) => Some(ScopeFlags::empty()), + _ => None, + } + } +} diff --git a/crates/oxc_semantic/src/scope/id.rs b/crates/oxc_semantic/src/scope/id.rs new file mode 100644 index 000000000..798a1bbfc --- /dev/null +++ b/crates/oxc_semantic/src/scope/id.rs @@ -0,0 +1,32 @@ +use std::ops::Deref; + +use indextree::NodeId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScopeId(NodeId); + +impl ScopeId { + #[must_use] + pub const fn new(node_id: NodeId) -> Self { + Self(node_id) + } + + #[must_use] + pub const fn indextree_id(&self) -> NodeId { + self.0 + } +} + +impl From for ScopeId { + fn from(node_id: NodeId) -> Self { + Self(node_id) + } +} + +impl Deref for ScopeId { + type Target = NodeId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/oxc_semantic/src/scope/mod.rs b/crates/oxc_semantic/src/scope/mod.rs new file mode 100644 index 000000000..a3de45b78 --- /dev/null +++ b/crates/oxc_semantic/src/scope/mod.rs @@ -0,0 +1,86 @@ +//! ECMAScript Scope Tree +//! See [Scope Analysis](https://tc39.es/ecma262/#sec-syntax-directed-operations-scope-analysis) +//! Code Adapted from [acorn](https://github.com/acornjs/acorn/blob/master/acorn/src/scope.js) +#![allow(non_upper_case_globals)] + +mod builder; +mod id; +mod tree; + +use bitflags::bitflags; +pub use builder::*; +pub use tree::ScopeTree; + +pub use self::id::ScopeId; + +#[derive(Debug, Clone)] +pub struct Scope { + /// [Strict Mode Code](https://tc39.es/ecma262/#sec-strict-mode-code) + /// [Use Strict Directive Prologue](https://tc39.es/ecma262/#sec-directive-prologues-and-the-use-strict-directive) + pub(crate) strict_mode: bool, + + pub flags: ScopeFlags, +} + +bitflags! { + #[derive(Default)] + pub struct ScopeFlags: u16 { + const Top = 1 << 0; + const Function = 1 << 1; + const Arrow = 1 << 2; + const ClassStaticBlock = 1 << 4; + const TsModuleBlock = 1 << 5; // `declare namespace` + const Constructor = 1 << 6; + const GetAccessor = 1 << 7; + const SetAccessor = 1 << 8; + const VAR = Self::Top.bits | Self::Function.bits | Self::ClassStaticBlock.bits | Self::TsModuleBlock.bits; + const MODIFIERS = Self::Constructor.bits | Self::GetAccessor.bits | Self::SetAccessor.bits; + } +} + +impl Scope { + #[must_use] + pub const fn new(flags: ScopeFlags, strict_mode: bool) -> Self { + Self { strict_mode, flags } + } + + #[must_use] + pub const fn strict_mode(&self) -> bool { + self.strict_mode + } + + #[must_use] + pub const fn is_top(&self) -> bool { + self.flags.intersects(ScopeFlags::Top) + } + + #[must_use] + pub const fn is_ts_module(&self) -> bool { + self.flags.intersects(ScopeFlags::TsModuleBlock) + } + + #[must_use] + pub const fn is_function(&self) -> bool { + self.flags.intersects(ScopeFlags::Function) + } + + #[must_use] + pub const fn is_static_block(&self) -> bool { + self.flags.intersects(ScopeFlags::ClassStaticBlock) + } + + #[must_use] + pub const fn is_constructor(&self) -> bool { + self.flags.intersects(ScopeFlags::Constructor) + } + + #[must_use] + pub const fn is_get_accessor(&self) -> bool { + self.flags.intersects(ScopeFlags::GetAccessor) + } + + #[must_use] + pub const fn is_set_accessor(&self) -> bool { + self.flags.intersects(ScopeFlags::SetAccessor) + } +} diff --git a/crates/oxc_semantic/src/scope/tree.rs b/crates/oxc_semantic/src/scope/tree.rs new file mode 100644 index 000000000..309fa6b82 --- /dev/null +++ b/crates/oxc_semantic/src/scope/tree.rs @@ -0,0 +1,111 @@ +use std::ops::{Deref, DerefMut, Index, IndexMut}; + +use indextree::{Ancestors, Arena, Node, NodeId}; + +use super::{Scope, ScopeFlags, ScopeId}; +use crate::node::AstNode; + +#[derive(Debug)] +pub struct ScopeTree { + scopes: Arena, + + root_scope_id: ScopeId, +} + +impl ScopeTree { + #[must_use] + pub fn new(root_strict_mode: bool) -> Self { + let mut scopes = Arena::new(); + let root_scope = Scope::new(ScopeFlags::Top, root_strict_mode); + let root_scope_id = scopes.new_node(root_scope).into(); + Self { scopes, root_scope_id } + } + + #[must_use] + pub const fn root_scope_id(&self) -> ScopeId { + self.root_scope_id + } + + #[must_use] + pub fn ancestors(&self, scope_id: ScopeId) -> Ancestors<'_, Scope> { + scope_id.ancestors(&self.scopes) + } + + #[must_use] + pub fn node_scope(&self, node: &AstNode) -> &Scope { + self.scopes[node.get().scope_id().indextree_id()].get() + } + + #[must_use] + pub fn node_scope_ancestors(&self, node: &AstNode) -> Ancestors<'_, Scope> { + self.ancestors(node.get().scope_id()) + } + + /// # Panics + /// When parent scope cannot be found, but this will not happen because + /// scopes are never removed. + #[must_use] + pub fn parent_node_id(&self, scope_id: ScopeId) -> NodeId { + self.scopes[*scope_id].parent().unwrap() + } + + #[must_use] + pub fn parent_scope(&self, scope_id: ScopeId) -> &Scope { + let parent_id = self.parent_node_id(scope_id); + self.scopes[parent_id].get() + } + + #[must_use] + pub fn parent_scope_mut(&mut self, scope_id: ScopeId) -> &mut Scope { + let parent_id = self.parent_node_id(scope_id); + self.scopes[parent_id].get_mut() + } + + #[must_use] + pub fn strict_mode(&self, node: &AstNode) -> bool { + let scope = self.node_scope(node); + node.get().strict_mode(scope) + } +} + +impl Index for ScopeTree { + type Output = Node; + + fn index(&self, id: NodeId) -> &Self::Output { + &self.scopes[id] + } +} + +impl IndexMut for ScopeTree { + fn index_mut(&mut self, id: NodeId) -> &mut Node { + &mut self.scopes[id] + } +} + +impl Index for ScopeTree { + type Output = Scope; + + fn index(&self, id: ScopeId) -> &Self::Output { + self.scopes[id.indextree_id()].get() + } +} + +impl IndexMut for ScopeTree { + fn index_mut(&mut self, id: ScopeId) -> &mut Scope { + self.scopes[id.indextree_id()].get_mut() + } +} + +impl Deref for ScopeTree { + type Target = Arena; + + fn deref(&self) -> &Self::Target { + &self.scopes + } +} + +impl DerefMut for ScopeTree { + fn deref_mut(&mut self) -> &mut Arena { + &mut self.scopes + } +} diff --git a/tasks/benchmark/src/main.rs b/tasks/benchmark/src/main.rs index 29195c12c..b06b51778 100644 --- a/tasks/benchmark/src/main.rs +++ b/tasks/benchmark/src/main.rs @@ -91,12 +91,13 @@ fn bench_semantic(criterion: &mut Criterion, codes: &[Code]) { &code.source_text, |b, source_text| { let allocator = Allocator::default(); - let ret = Parser::new(&allocator, source_text, SourceType::default()).parse(); + let source_type = SourceType::from_path(&code.file_name).unwrap(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); let program = allocator.alloc(ret.program); let trivias = Rc::new(ret.trivias); b.iter(|| { - let _semantic = - SemanticBuilder::new().build(black_box(program), trivias.clone()); + let _semantic = SemanticBuilder::new(source_type) + .build(black_box(program), trivias.clone()); }); }, );