Skip to main content
Oxc’s semantic analyzer performs symbol resolution, scope analysis, and semantic validation on an AST produced by the parser.

Features

  • Symbol table construction
  • Scope tree building
  • Reference resolution
  • Control Flow Graph (CFG) generation
  • Type binding information
  • JSDoc parsing
  • Additional syntax error checking

Installation

Add to your Cargo.toml:
[dependencies]
oxc_semantic = "0.116.0"
oxc_parser = "0.116.0"
oxc_allocator = "0.116.0"
oxc_ast = "0.116.0"
oxc_span = "0.116.0"

Basic Usage

use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;

let allocator = Allocator::default();
let source = "let x = 1; x + 2;";
let source_type = SourceType::default();

// Parse the source
let parsed = Parser::new(&allocator, source, source_type).parse();

// Build semantic information
let semantic_ret = SemanticBuilder::new()
    .with_check_syntax_error(true)
    .build(&parsed.program);

if semantic_ret.errors.is_empty() {
    let semantic = semantic_ret.semantic;
    println!("Semantic analysis successful!");
    println!("Found {} symbols", semantic.scoping().symbols_len());
}

Core Types

SemanticBuilder

SemanticBuilder
struct
Builder for constructing semantic analysis results.
Methods:
impl SemanticBuilder {
    pub fn new() -> Self;
    
    pub fn with_check_syntax_error(mut self, yes: bool) -> Self;
    
    pub fn with_cfg(mut self, yes: bool) -> Self;
    
    pub fn with_excess_capacity(mut self, capacity: f64) -> Self;
    
    pub fn build(self, program: &Program) -> SemanticBuilderReturn;
}
with_check_syntax_error
method
Enable additional syntax error checking beyond what the parser does. Recommended for production use.
with_cfg
method
Enable Control Flow Graph generation. Requires the cfg feature.
with_excess_capacity
method
Pre-allocate extra capacity for symbols/scopes/references. Useful before transformations that create new bindings. Pass 2.0 to triple capacity.

Semantic

Semantic
struct
Contains the results of semantic analysis including scopes, symbols, and references.
Key Methods:
impl Semantic<'a> {
    // Scoping information
    pub fn scoping(&self) -> &Scoping;
    pub fn scoping_mut(&mut self) -> &mut Scoping;
    
    // AST nodes with parent information
    pub fn nodes(&self) -> &AstNodes<'a>;
    
    // Symbol operations
    pub fn symbol_declaration(&self, symbol_id: SymbolId) -> &AstNode<'a>;
    pub fn symbol_references(&self, symbol_id: SymbolId) -> impl Iterator<Item = &Reference>;
    pub fn symbol_scope(&self, symbol_id: SymbolId) -> ScopeId;
    
    // Reference checking
    pub fn is_unresolved_reference(&self, node_id: NodeId) -> bool;
    pub fn is_reference_to_global_variable(&self, ident: &IdentifierReference) -> bool;
    
    // Comments
    pub fn comments(&self) -> &[Comment];
    pub fn has_comments_between(&self, span: Span) -> bool;
    
    // Source information
    pub fn source_text(&self) -> &'a str;
    pub fn source_type(&self) -> &SourceType;
}

Scoping

Scoping
struct
Symbol table, scope tree, and reference information.
Key Methods:
impl Scoping {
    // Scope operations
    pub fn root_scope_id(&self) -> ScopeId;
    pub fn scopes_len(&self) -> usize;
    
    // Symbol operations  
    pub fn symbols_len(&self) -> usize;
    pub fn symbol_name(&self, symbol_id: SymbolId) -> &str;
    pub fn symbol_flags(&self, symbol_id: SymbolId) -> SymbolFlags;
    pub fn symbol_span(&self, symbol_id: SymbolId) -> Span;
    
    // Binding lookup
    pub fn get_binding(&self, scope_id: ScopeId, name: &str) -> Option<SymbolId>;
    pub fn get_root_binding(&self, name: &str) -> Option<SymbolId>;
    
    // Reference operations
    pub fn get_resolved_references(&self, symbol_id: SymbolId) -> impl Iterator<Item = &Reference>;
    pub fn get_reference(&self, reference_id: ReferenceId) -> &Reference;
    
    // Unresolved references
    pub fn root_unresolved_references(&self) -> &FxHashMap<Atom, Vec<ReferenceId>>;
}

AstNodes

AstNodes
struct
Parent-pointing tree of all AST nodes.
impl AstNodes<'a> {
    pub fn get_node(&self, node_id: NodeId) -> &AstNode<'a>;
    pub fn parent_node(&self, node_id: NodeId) -> Option<&AstNode<'a>>;
    pub fn iter(&self) -> impl Iterator<Item = &AstNode<'a>>;
}

Examples

Finding All Variable References

let allocator = Allocator::default();
let source = r#"
    let count = 0;
    count++;
    console.log(count);
"#;

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;

let scoping = semantic.scoping();

// Find the 'count' symbol
if let Some(symbol_id) = scoping.get_root_binding("count") {
    println!("Symbol 'count' declared at: {:?}", scoping.symbol_span(symbol_id));
    
    // Get all references to 'count'
    for reference in semantic.symbol_references(symbol_id) {
        let span = semantic.reference_span(reference);
        let is_write = reference.is_write();
        println!("  Referenced at {:?}, write={}", span, is_write);
    }
}

Checking for Undeclared Variables

let allocator = Allocator::default();
let source = "console.log(undeclaredVar);";

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;

// Check for unresolved references
for (name, reference_ids) in semantic.scoping().root_unresolved_references() {
    println!("Undeclared variable: {}", name);
    for reference_id in reference_ids {
        let reference = semantic.scoping().get_reference(*reference_id);
        let node = semantic.nodes().get_node(reference.node_id());
        println!("  Used at: {:?}", node.kind().span());
    }
}

Walking the Scope Tree

let allocator = Allocator::default();
let source = r#"
    let outer = 1;
    function foo() {
        let inner = 2;
        return outer + inner;
    }
"#;

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;

fn print_scope(scoping: &Scoping, scope_id: ScopeId, indent: usize) {
    let prefix = "  ".repeat(indent);
    println!("{}Scope {:?} with {} bindings", prefix, scope_id, 
             scoping.scope_bindings(scope_id).count());
    
    for child_scope_id in scoping.child_scopes(scope_id) {
        print_scope(scoping, *child_scope_id, indent + 1);
    }
}

let root = semantic.scoping().root_scope_id();
print_scope(semantic.scoping(), root, 0);

Using with Control Flow Graph

Requires the cfg feature:
oxc_semantic = { version = "0.116.0", features = ["cfg"] }
use oxc_semantic::SemanticBuilder;

let allocator = Allocator::default();
let source = r#"
    function test(x) {
        if (x > 0) {
            return true;
        }
        return false;
    }
"#;

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new()
    .with_cfg(true)
    .build(&parsed.program)
    .semantic;

if let Some(cfg) = semantic.cfg() {
    println!("Control flow graph has {} basic blocks", cfg.basic_blocks_len());
}

Iterating Over All Nodes

use oxc_ast::AstKind;

let allocator = Allocator::default();
let source = "const x = 1; debugger; x + 2;";

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;

// Iterate through all nodes in the AST
for node in semantic.nodes().iter() {
    match node.kind() {
        AstKind::DebuggerStatement(stmt) => {
            println!("Found debugger statement at {:?}", stmt.span);
        }
        AstKind::IdentifierReference(ident) => {
            let is_global = semantic.is_reference_to_global_variable(ident);
            println!("Identifier '{}' is global: {}", ident.name, is_global);
        }
        _ => {}
    }
}

Symbol and Reference Flags

SymbolFlags

Indicates what kind of binding a symbol represents:
use oxc_syntax::symbol::SymbolFlags;

let flags = scoping.symbol_flags(symbol_id);

if flags.contains(SymbolFlags::FunctionScopedVariable) {
    println!("This is a var declaration");
}

if flags.contains(SymbolFlags::BlockScopedVariable) {
    println!("This is a let/const declaration");
}

if flags.contains(SymbolFlags::Function) {
    println!("This is a function");
}

ReferenceFlags

Indicates how a reference is used:
use oxc_syntax::reference::ReferenceFlags;

for reference in semantic.symbol_references(symbol_id) {
    if reference.is_read() {
        println!("Symbol is read");
    }
    
    if reference.is_write() {
        println!("Symbol is written to");
    }
    
    if reference.is_read() && reference.is_write() {
        println!("Symbol is read and written (e.g., x++)");
    }
}

JSDoc Support

Requires the jsdoc feature:
oxc_semantic = { version = "0.116.0", features = ["jsdoc"] }
let allocator = Allocator::default();
let source = r#"
    /**
     * Add two numbers
     * @param {number} a - First number
     * @param {number} b - Second number
     * @returns {number} Sum of a and b
     */
    function add(a, b) {
        return a + b;
    }
"#;

let parsed = Parser::new(&allocator, source, SourceType::default()).parse();
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;

// Access JSDoc comments
if let Some(jsdoc) = semantic.jsdoc().get_function_jsdoc(/* function node */) {
    for tag in &jsdoc.tags {
        println!("JSDoc tag: {:?}", tag);
    }
}

Feature Flags

cfg
feature
Enable Control Flow Graph generation. Adds oxc_cfg dependency.
jsdoc
feature
Enable JSDoc comment parsing. Adds oxc_jsdoc dependency.
linter
feature
Enable additional data structures used by oxc_linter. Includes jsdoc feature.
serialize
feature
Enable serialization support for semantic data structures.

Performance Tips

  • Use with_excess_capacity() before transformations to avoid reallocations
  • The Semantic struct owns all scoping data - extract what you need with into_scoping()
  • AST node iteration via nodes() is very fast - use it instead of recursive visitors when possible

API Documentation

For complete API documentation, see docs.rs/oxc_semantic.

Build docs developers (and LLMs) love