Semantic analysis is the process of understanding the meaning and relationships within code beyond its syntax. While the parser produces an AST representing the code’s structure, the semantic analyzer builds additional data structures that capture scoping, symbol resolution, and control flow.
Oxc’s semantic analyzer performs comprehensive analysis of JavaScript and TypeScript programs, building:
Symbol Table
All declared identifiers (variables, functions, classes) with their properties
Scope Tree
Nested scopes following ECMAScript scoping rules
Reference Graph
Links between identifier uses and their declarations
Semantic analysis bridges the gap between syntax and meaning. It enables tools like linters to detect issues like undefined variables, and transformers to safely rename identifiers.
Different language constructs create different types of scopes:
// Program scope (top-level)const global = 1;// Function scopefunction outer() { var functionScoped = 2; // Block scope if (true) { let blockScoped = 3; const alsoBlockScoped = 4; } // Another function scope function inner() { let nested = 5; }}// Class scopeclass MyClass { method() { let methodScope = 6; }}
Each scope knows:
Its parent scope
What bindings (variables) it declares
Its scope flags (function, block, strict mode, etc.)
use oxc_syntax::scope::{ScopeFlags, ScopeId};let scoping = semantic.semantic.scoping();// Iterate over all scopesfor scope_id in scoping.scope_ids() { let flags = scoping.get_flags(scope_id); println!("Scope {:?}:", scope_id); println!(" Is function scope: {}", flags.contains(ScopeFlags::Function)); println!(" Is strict mode: {}", flags.contains(ScopeFlags::StrictMode)); // Get parent scope if let Some(parent_id) = scoping.get_parent_id(scope_id) { println!(" Parent: {:?}", parent_id); } // Get bindings in this scope if let Some(bindings) = scoping.get_bindings(scope_id) { for (name, symbol_id) in bindings { println!(" Binding: {} -> {:?}", name, symbol_id); } }}
Flags: Type of declaration (var, let, const, function, class, etc.)
Scope: Which scope it belongs to
References: All places it’s used
use oxc_syntax::symbol::{SymbolFlags, SymbolId};let scoping = semantic.semantic.scoping();// Iterate over all symbolsfor symbol_id in scoping.symbol_ids() { let name = scoping.symbol_name(symbol_id); let flags = scoping.symbol_flags(symbol_id); let span = scoping.symbol_span(symbol_id); let scope_id = scoping.symbol_scope(symbol_id); println!("Symbol: {}", name); println!(" ID: {:?}", symbol_id); println!(" Flags: {:?}", flags); println!(" Declared at: {:?}", span); println!(" Scope: {:?}", scope_id); // Check symbol type if flags.contains(SymbolFlags::BlockScopedVariable) { println!(" This is a let/const variable"); } if flags.contains(SymbolFlags::Function) { println!(" This is a function"); }}
These flags help tools understand how identifiers behave. For example, a linter checking for variable reassignment needs to know if something is declared with const.
const x = 42; // Symbol: x (declaration)function foo() { console.log(x); // Reference: x -> Symbol x (read) const y = x + 1; // Reference: x -> Symbol x (read) return y; // Reference: y -> Symbol y (read)}foo(); // Reference: foo -> Symbol foo (read)
use oxc_syntax::reference::{ReferenceFlags, ReferenceId};let scoping = semantic.semantic.scoping();// Get all references for a symbolfor symbol_id in scoping.symbol_ids() { let symbol_name = scoping.symbol_name(symbol_id); println!("Symbol '{}' is referenced at:", symbol_name); for reference_id in scoping.get_resolved_reference_ids(symbol_id) { let reference = scoping.get_reference(*reference_id); let flags = reference.flags(); if flags.contains(ReferenceFlags::Read) { println!(" - Read reference"); } if flags.contains(ReferenceFlags::Write) { println!(" - Write reference"); } }}
// Find references to undefined variableslet unresolved = scoping.root_unresolved_references();for (name, reference_ids) in unresolved { println!("Unresolved reference: '{}'", name); for reference_id in reference_ids { let reference = scoping.get_reference(*reference_id); println!(" Used at node {:?}", reference.node_id()); }}
For code like:
function foo() { console.log(undefinedVar); // undefinedVar is not declared}
// After semantic analysis, declarations have symbol_id populatedif let Statement::VariableDeclaration(decl) = stmt { for declarator in &decl.declarations { if let BindingPatternKind::BindingIdentifier(ident) = &declarator.id.kind { // This is now Some(SymbolId) after semantic analysis if let Some(symbol_id) = ident.symbol_id.get() { println!("Variable '{}' has symbol ID {:?}", ident.name, symbol_id); } } }}
// References have reference_id populatedif let Expression::Identifier(ident) = expr { if let Some(reference_id) = ident.reference_id.get() { let reference = scoping.get_reference(reference_id); println!("Reference '{}':", ident.name); println!(" Flags: {:?}", reference.flags()); // Find what it refers to if let Some(symbol_id) = reference.symbol_id() { let symbol_name = scoping.symbol_name(symbol_id); println!(" Resolves to symbol '{}'", symbol_name); } }}
pub struct Scoping { // Symbol table (stored as struct-of-arrays for efficiency) symbol_table: SymbolTable, // All references in the program references: IndexVec<ReferenceId, Reference>, // Scope tree (stored as struct-of-arrays) scope_table: ScopeTable, // Inner data stored in arena allocator cell: ScopingCell,}
Struct-of-Arrays Design: Instead of Vec<Symbol> where each Symbol is a struct with multiple fields, Oxc uses separate vectors for each field. This improves cache locality and reduces memory overhead.
struct ScopeTable { parent_ids: Vec<Option<ScopeId>>, // Parent scope for each scope node_ids: Vec<NodeId>, // AST node that created the scope flags: Vec<ScopeFlags>, // Scope type and properties}
struct SymbolTable { symbol_spans: Vec<Span>, // Where declared symbol_flags: Vec<SymbolFlags>, // Type of declaration symbol_scope_ids: Vec<ScopeId>, // Which scope it's in symbol_declarations: Vec<NodeId>, // AST node of declaration}
This layout is more cache-friendly than Vec<Symbol> because related data is stored contiguously.