Skip to main content

oxc_semantic/
lib.rs

1//! Semantic analysis of a JavaScript/TypeScript program.
2//!
3//! # Example
4//! ```ignore
5#![doc = include_str!("../examples/semantic.rs")]
6//! ```
7
8use std::ops::RangeBounds;
9
10use oxc_ast::{
11    AstKind, Comment, CommentsRange, ast::IdentifierReference, comments_range, get_comment_at,
12    has_comments_between, is_inside_comment,
13};
14#[cfg(feature = "cfg")]
15use oxc_cfg::ControlFlowGraph;
16use oxc_span::{GetSpan, SourceType, Span};
17// Re-export flags and ID types
18pub use oxc_syntax::{
19    node::{NodeFlags, NodeId},
20    reference::{Reference, ReferenceFlags, ReferenceId},
21    scope::{ScopeFlags, ScopeId},
22    symbol::{SymbolFlags, SymbolId},
23};
24
25#[cfg(feature = "cfg")]
26pub mod dot;
27
28#[cfg(feature = "linter")]
29mod ast_types_bitset;
30mod binder;
31mod builder;
32mod checker;
33mod class;
34mod diagnostics;
35mod is_global_reference;
36#[cfg(feature = "jsdoc")]
37mod jsdoc;
38mod label;
39mod multi_index_vec;
40mod node;
41mod scoping;
42mod stats;
43mod unresolved_stack;
44
45#[cfg(feature = "linter")]
46pub use ast_types_bitset::AstTypesBitset;
47pub use builder::{SemanticBuilder, SemanticBuilderReturn};
48pub use is_global_reference::IsGlobalReference;
49#[cfg(feature = "jsdoc")]
50pub use jsdoc::JSDocFinder;
51pub use node::{AstNode, AstNodes};
52#[cfg(feature = "jsdoc")]
53pub use oxc_jsdoc::{JSDoc, JSDocTag};
54pub use scoping::Scoping;
55pub use stats::Stats;
56
57use class::ClassTable;
58
59/// Semantic analysis of a JavaScript/TypeScript program.
60///
61/// [`Semantic`] contains the results of analyzing a program, including the
62/// [`Abstract Syntax Tree (AST)`], [`scoping`], and [`control flow graph (CFG)`].
63///
64/// Do not construct this struct directly; instead, use [`SemanticBuilder`].
65///
66/// [`Abstract Syntax Tree (AST)`]: crate::AstNodes
67/// [`scoping`]: crate::Scoping
68/// [`control flow graph (CFG)`]: crate::ControlFlowGraph
69#[derive(Default)]
70pub struct Semantic<'a> {
71    /// Source code of the JavaScript/TypeScript program being analyzed.
72    source_text: &'a str,
73
74    /// What kind of source code is being analyzed. Comes from the parser.
75    source_type: SourceType,
76
77    /// The Abstract Syntax Tree (AST) nodes.
78    nodes: AstNodes<'a>,
79
80    scoping: Scoping,
81
82    classes: ClassTable<'a>,
83
84    /// Parsed comments.
85    comments: &'a [Comment],
86    irregular_whitespaces: Box<[Span]>,
87
88    /// Parsed JSDoc comments.
89    #[cfg(feature = "jsdoc")]
90    jsdoc: JSDocFinder<'a>,
91
92    unused_labels: Vec<NodeId>,
93
94    /// Control flow graph. Only present if [`Semantic`] is built with cfg
95    /// creation enabled using [`SemanticBuilder::with_cfg`].
96    #[cfg(feature = "cfg")]
97    cfg: Option<ControlFlowGraph>,
98    #[cfg(not(feature = "cfg"))]
99    #[expect(unused)]
100    cfg: (),
101}
102
103impl<'a> Semantic<'a> {
104    /// Extract [`Scoping`] from [`Semantic`].
105    pub fn into_scoping(self) -> Scoping {
106        self.scoping
107    }
108
109    /// Extract [`Scoping`] and [`AstNode`] from the [`Semantic`].
110    pub fn into_scoping_and_nodes(self) -> (Scoping, AstNodes<'a>) {
111        (self.scoping, self.nodes)
112    }
113
114    /// Source code of the JavaScript/TypeScript program being analyzed.
115    pub fn source_text(&self) -> &'a str {
116        self.source_text
117    }
118
119    /// What kind of source code is being analyzed. Comes from the parser.
120    pub fn source_type(&self) -> &SourceType {
121        &self.source_type
122    }
123
124    /// Nodes in the Abstract Syntax Tree (AST)
125    pub fn nodes(&self) -> &AstNodes<'a> {
126        &self.nodes
127    }
128
129    /// Scoping data collected for this program.
130    pub fn scoping(&self) -> &Scoping {
131        &self.scoping
132    }
133
134    /// Mutable access to scoping data.
135    pub fn scoping_mut(&mut self) -> &mut Scoping {
136        &mut self.scoping
137    }
138
139    /// Mutable access to scoping data together with read-only AST nodes.
140    pub fn scoping_mut_and_nodes(&mut self) -> (&mut Scoping, &AstNodes<'a>) {
141        (&mut self.scoping, &self.nodes)
142    }
143
144    /// Class metadata collected during semantic analysis.
145    pub fn classes(&self) -> &ClassTable<'_> {
146        &self.classes
147    }
148
149    /// Set recorded spans for irregular unicode whitespace in source text.
150    pub fn set_irregular_whitespaces(&mut self, irregular_whitespaces: Box<[Span]>) {
151        self.irregular_whitespaces = irregular_whitespaces;
152    }
153
154    /// Trivias (comments) found while parsing
155    pub fn comments(&self) -> &[Comment] {
156        self.comments
157    }
158
159    /// Iterate comments within a byte range.
160    pub fn comments_range<R>(&self, range: R) -> CommentsRange<'_>
161    where
162        R: RangeBounds<u32>,
163    {
164        comments_range(self.comments, range)
165    }
166
167    /// Returns `true` if any comment lies between `span.start` and `span.end`.
168    pub fn has_comments_between(&self, span: Span) -> bool {
169        has_comments_between(self.comments, span)
170    }
171
172    /// Returns `true` if `pos` is inside a parsed comment.
173    pub fn is_inside_comment(&self, pos: u32) -> bool {
174        is_inside_comment(self.comments, pos)
175    }
176
177    /// Get the comment containing a position, if any.
178    pub fn get_comment_at(&self, pos: u32) -> Option<&Comment> {
179        get_comment_at(self.comments, pos)
180    }
181
182    /// Spans of irregular whitespace discovered by the parser.
183    pub fn irregular_whitespaces(&self) -> &[Span] {
184        &self.irregular_whitespaces
185    }
186
187    /// Parsed [`JSDoc`] comments.
188    ///
189    /// Will be empty if JSDoc parsing is disabled.
190    #[cfg(feature = "jsdoc")]
191    pub fn jsdoc(&self) -> &JSDocFinder<'a> {
192        &self.jsdoc
193    }
194
195    /// Labels that were declared but never used.
196    pub fn unused_labels(&self) -> &Vec<NodeId> {
197        &self.unused_labels
198    }
199
200    /// Control flow graph.
201    ///
202    /// Only present if [`Semantic`] is built with cfg creation enabled using
203    /// [`SemanticBuilder::with_cfg`].
204    #[cfg(feature = "cfg")]
205    pub fn cfg(&self) -> Option<&ControlFlowGraph> {
206        self.cfg.as_ref()
207    }
208
209    /// Control flow graph.
210    ///
211    /// Always returns `None` when the `cfg` feature is disabled.
212    #[cfg(not(feature = "cfg"))]
213    #[expect(clippy::unused_self)]
214    pub fn cfg(&self) -> Option<&()> {
215        None
216    }
217
218    /// Get statistics about data held in `Semantic`.
219    pub fn stats(&self) -> Stats {
220        #[expect(clippy::cast_possible_truncation)]
221        Stats::new(
222            self.nodes.len() as u32,
223            self.scoping.scopes_len() as u32,
224            self.scoping.symbols_len() as u32,
225            self.scoping.references.len() as u32,
226        )
227    }
228
229    /// Returns `true` if `node_id` points to an unresolved identifier reference.
230    pub fn is_unresolved_reference(&self, node_id: NodeId) -> bool {
231        let reference_node = self.nodes.get_node(node_id);
232        let AstKind::IdentifierReference(id) = reference_node.kind() else {
233            return false;
234        };
235        self.scoping.root_unresolved_references().contains_key(&id.name)
236    }
237
238    /// Find which scope a symbol is declared in
239    pub fn symbol_scope(&self, symbol_id: SymbolId) -> ScopeId {
240        self.scoping.symbol_scope_id(symbol_id)
241    }
242
243    /// Get all resolved references for a symbol
244    pub fn symbol_references(
245        &self,
246        symbol_id: SymbolId,
247    ) -> impl Iterator<Item = &Reference> + '_ + use<'_> {
248        self.scoping.get_resolved_references(symbol_id)
249    }
250
251    /// Get the AST node that declares `symbol_id`.
252    pub fn symbol_declaration(&self, symbol_id: SymbolId) -> &AstNode<'a> {
253        self.nodes.get_node(self.scoping.symbol_declaration(symbol_id))
254    }
255
256    /// Returns `true` if `ident` resolves to a global (unbound) reference.
257    pub fn is_reference_to_global_variable(&self, ident: &IdentifierReference) -> bool {
258        self.scoping.root_unresolved_references().contains_key(&ident.name)
259    }
260
261    /// Get the textual name for a semantic reference.
262    pub fn reference_name(&self, reference: &Reference) -> &str {
263        let node = self.nodes.get_node(reference.node_id());
264        match node.kind() {
265            AstKind::IdentifierReference(id) => id.name.as_str(),
266            _ => unreachable!(),
267        }
268    }
269
270    /// Get the source span for a semantic reference.
271    pub fn reference_span(&self, reference: &Reference) -> Span {
272        let node = self.nodes.get_node(reference.node_id());
273        node.kind().span()
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use oxc_allocator::Allocator;
280    use oxc_ast::{AstKind, ast::VariableDeclarationKind};
281    use oxc_span::{Ident, SourceType, Str};
282
283    use super::*;
284
285    /// Create a [`Semantic`] from source code, assuming there are no syntax/semantic errors.
286    fn get_semantic<'s, 'a: 's>(
287        allocator: &'a Allocator,
288        source: &'s str,
289        source_type: SourceType,
290    ) -> Semantic<'s> {
291        let parse = oxc_parser::Parser::new(allocator, source, source_type).parse();
292        assert!(parse.errors.is_empty());
293        let semantic = SemanticBuilder::new().build(allocator.alloc(parse.program));
294        assert!(semantic.errors.is_empty(), "Parse error: {}", semantic.errors[0]);
295        semantic.semantic
296    }
297
298    #[test]
299    fn test_symbols() {
300        let source = "
301            let a;
302            function foo(a) {
303                return a + 1;
304            }
305            let b = a + foo(1);";
306        let allocator = Allocator::default();
307        let semantic = get_semantic(&allocator, source, SourceType::default());
308
309        let top_level_a = semantic
310            .scoping()
311            .get_binding(semantic.scoping().root_scope_id(), Ident::new_const("a"))
312            .unwrap();
313
314        let decl = semantic.symbol_declaration(top_level_a);
315        match decl.kind() {
316            AstKind::VariableDeclarator(decl) => {
317                assert_eq!(decl.kind, VariableDeclarationKind::Let);
318            }
319            kind => panic!("Expected VariableDeclarator for 'let', got {kind:?}"),
320        }
321
322        let references = semantic.symbol_references(top_level_a);
323        assert_eq!(references.count(), 1);
324    }
325
326    #[test]
327    fn test_top_level_symbols() {
328        let source = "function Fn() {}";
329        let allocator = Allocator::default();
330        let semantic = get_semantic(&allocator, source, SourceType::default());
331        let scopes = semantic.scoping();
332
333        assert!(scopes.get_binding(scopes.root_scope_id(), Ident::new_const("Fn")).is_some());
334    }
335
336    #[test]
337    fn test_is_global() {
338        let source = "
339            var a = 0;
340            function foo() {
341            a += 1;
342            }
343
344            var b = a + 2;
345        ";
346        let allocator = Allocator::default();
347        let semantic = get_semantic(&allocator, source, SourceType::default());
348        for node in semantic.nodes() {
349            if let AstKind::IdentifierReference(id) = node.kind() {
350                assert!(!semantic.is_reference_to_global_variable(id));
351            }
352        }
353    }
354
355    #[test]
356    fn type_alias_gets_reference() {
357        let source = "type A = 1; type B = A";
358        let allocator = Allocator::default();
359        let source_type: SourceType = SourceType::default().with_typescript(true);
360        let semantic = get_semantic(&allocator, source, source_type);
361        assert_eq!(semantic.scoping().references.len(), 1);
362    }
363
364    #[test]
365    fn test_reference_resolutions_simple_read_write() {
366        let alloc = Allocator::default();
367        let target_symbol_name = Str::from("a");
368        let typescript = SourceType::ts();
369        let sources = [
370            // simple cases
371            (SourceType::default(), "let a = 1; a = 2", ReferenceFlags::write()),
372            (SourceType::default(), "let a = 1, b; b = a", ReferenceFlags::read()),
373            (SourceType::default(), "let a = 1, b; b[a]", ReferenceFlags::read()),
374            (SourceType::default(), "let a = 1, b = 1, c; c = a + b", ReferenceFlags::read()),
375            (SourceType::default(), "function a() { return }; a()", ReferenceFlags::read()),
376            (SourceType::default(), "class a {}; new a()", ReferenceFlags::read()),
377            (SourceType::default(), "let a; function foo() { return a }", ReferenceFlags::read()),
378            // pattern assignment
379            (SourceType::default(), "let a = 1, b; b = { a }", ReferenceFlags::read()),
380            (SourceType::default(), "let a, b; ({ b } = { a })", ReferenceFlags::read()),
381            (SourceType::default(), "let a, b; ({ a } = { b })", ReferenceFlags::write()),
382            (SourceType::default(), "let a, b; ([ b ] = [ a ])", ReferenceFlags::read()),
383            (SourceType::default(), "let a, b; ([ a ] = [ b ])", ReferenceFlags::write()),
384            // property access/mutation
385            (SourceType::default(), "let a = { b: 1 }; a.b = 2", ReferenceFlags::read()),
386            (SourceType::default(), "let a = { b: 1 }; a.b += 2", ReferenceFlags::read()),
387            // parens are pass-through
388            (SourceType::default(), "let a = 1, b; b = (a)", ReferenceFlags::read()),
389            (SourceType::default(), "let a = 1, b; b = ++(a)", ReferenceFlags::read_write()),
390            (SourceType::default(), "let a = 1, b; b = ++((((a))))", ReferenceFlags::read_write()),
391            (SourceType::default(), "let a = 1, b; b = ((++((a))))", ReferenceFlags::read_write()),
392            // simple binops/calls for sanity check
393            (SourceType::default(), "let a, b; a + b", ReferenceFlags::read()),
394            (SourceType::default(), "let a, b; b(a)", ReferenceFlags::read()),
395            (SourceType::default(), "let a, b; a = 5", ReferenceFlags::write()),
396            // unary op counts as write, but checking continues up tree
397            (SourceType::default(), "let a = 1, b; b = ++a", ReferenceFlags::read_write()),
398            (SourceType::default(), "let a = 1, b; b = --a", ReferenceFlags::read_write()),
399            (SourceType::default(), "let a = 1, b; b = a++", ReferenceFlags::read_write()),
400            (SourceType::default(), "let a = 1, b; b = a--", ReferenceFlags::read_write()),
401            // assignment expressions count as read-write
402            (SourceType::default(), "let a = 1, b; b = a += 5", ReferenceFlags::read_write()),
403            (SourceType::default(), "let a = 1; a += 5", ReferenceFlags::read_write()),
404            (SourceType::default(), "let a, b; b = a = 1", ReferenceFlags::write()),
405            (SourceType::default(), "let a, b; b = (a = 1)", ReferenceFlags::write()),
406            (SourceType::default(), "let a, b, c; b = c = a", ReferenceFlags::read()),
407            // sequences return last read_write in sequence
408            (SourceType::default(), "let a, b; b = (0, a++)", ReferenceFlags::read_write()),
409            // loops
410            (
411                SourceType::default(),
412                "var a, arr = [1, 2, 3]; for(a in arr) { break }",
413                ReferenceFlags::write(),
414            ),
415            (
416                SourceType::default(),
417                "var a, obj = { }; for(a of obj) { break }",
418                ReferenceFlags::write(),
419            ),
420            (SourceType::default(), "var a; for(; false; a++) { }", ReferenceFlags::read_write()),
421            (SourceType::default(), "var a = 1; while(a < 5) { break }", ReferenceFlags::read()),
422            // if statements
423            (
424                SourceType::default(),
425                "let a; if (a) { true } else { false }",
426                ReferenceFlags::read(),
427            ),
428            (
429                SourceType::default(),
430                "let a, b; if (a == b) { true } else { false }",
431                ReferenceFlags::read(),
432            ),
433            (
434                SourceType::default(),
435                "let a, b; if (b == a) { true } else { false }",
436                ReferenceFlags::read(),
437            ),
438            // identifiers not in last read_write are also considered a read (at
439            // least, or now)
440            (SourceType::default(), "let a, b; b = (a, 0)", ReferenceFlags::read()),
441            (SourceType::default(), "let a, b; b = (--a, 0)", ReferenceFlags::read_write()),
442            (
443                SourceType::default(),
444                "let a; function foo(a) { return a }; foo(a = 1)",
445                //                                        ^ write
446                ReferenceFlags::write(),
447            ),
448            // member expression
449            (SourceType::default(), "let a; a.b = 1", ReferenceFlags::read()),
450            (SourceType::default(), "let a; let b; b[a += 1] = 1", ReferenceFlags::read_write()),
451            (
452                SourceType::default(),
453                "let a; let b; let c; b[c[a = c['a']] = 'c'] = 'b'",
454                //                        ^ write
455                ReferenceFlags::write(),
456            ),
457            (
458                SourceType::default(),
459                "let a; let b; let c; a[c[b = c['a']] = 'c'] = 'b'",
460                ReferenceFlags::read(),
461            ),
462            (SourceType::default(), "console.log;let a=0;a++", ReferenceFlags::read_write()),
463            //                                           ^^^ UpdateExpression is always a read | write
464            // typescript
465            (typescript, "let a: number = 1; (a as any) = true", ReferenceFlags::write()),
466            (typescript, "let a: number = 1; a = true as any", ReferenceFlags::write()),
467            (typescript, "let a: number = 1; a = 2 as const", ReferenceFlags::write()),
468            (typescript, "let a: number = 1; a = 2 satisfies number", ReferenceFlags::write()),
469            (typescript, "let a: number; (a as any) = 1;", ReferenceFlags::write()),
470        ];
471
472        for (source_type, source, flags) in sources {
473            let semantic = get_semantic(&alloc, source, source_type);
474            let a_id = semantic
475                .scoping()
476                .get_root_binding(target_symbol_name.into())
477                .unwrap_or_else(|| {
478                    panic!("no references for '{target_symbol_name}' found");
479                });
480            let a_refs: Vec<_> = semantic.symbol_references(a_id).collect();
481            let num_refs = a_refs.len();
482
483            assert!(
484                num_refs == 1,
485                "expected to find 1 reference to '{target_symbol_name}' but {num_refs} were found\n\nsource:\n{source}"
486            );
487            let ref_type = a_refs[0];
488            if flags.is_write() {
489                assert!(
490                    ref_type.is_write(),
491                    "expected reference to '{target_symbol_name}' to be write\n\nsource:\n{source}"
492                );
493            } else {
494                assert!(
495                    !ref_type.is_write(),
496                    "expected reference to '{target_symbol_name}' not to have been written to, but it is\n\nsource:\n{source}"
497                );
498            }
499            if flags.is_read() {
500                assert!(
501                    ref_type.is_read(),
502                    "expected reference to '{target_symbol_name}' to be read\n\nsource:\n{source}"
503                );
504            } else {
505                assert!(
506                    !ref_type.is_read(),
507                    "expected reference to '{target_symbol_name}' not to be read, but it is\n\nsource:\n{source}"
508                );
509            }
510        }
511    }
512}