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