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