1#![doc = include_str!("../examples/semantic.rs")]
6use 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};
17pub 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#[derive(Default)]
70pub struct Semantic<'a> {
71 source_text: &'a str,
73
74 source_type: SourceType,
76
77 nodes: AstNodes<'a>,
79
80 scoping: Scoping,
81
82 classes: ClassTable<'a>,
83
84 comments: &'a [Comment],
86 irregular_whitespaces: Box<[Span]>,
87
88 #[cfg(feature = "jsdoc")]
90 jsdoc: JSDocFinder<'a>,
91
92 unused_labels: Vec<NodeId>,
93
94 #[cfg(feature = "cfg")]
97 cfg: Option<ControlFlowGraph>,
98 #[cfg(not(feature = "cfg"))]
99 #[expect(unused)]
100 cfg: (),
101}
102
103impl<'a> Semantic<'a> {
104 pub fn into_scoping(self) -> Scoping {
106 self.scoping
107 }
108
109 pub fn into_scoping_and_nodes(self) -> (Scoping, AstNodes<'a>) {
111 (self.scoping, self.nodes)
112 }
113
114 pub fn source_text(&self) -> &'a str {
116 self.source_text
117 }
118
119 pub fn source_type(&self) -> &SourceType {
121 &self.source_type
122 }
123
124 pub fn nodes(&self) -> &AstNodes<'a> {
126 &self.nodes
127 }
128
129 pub fn scoping(&self) -> &Scoping {
131 &self.scoping
132 }
133
134 pub fn scoping_mut(&mut self) -> &mut Scoping {
136 &mut self.scoping
137 }
138
139 pub fn scoping_mut_and_nodes(&mut self) -> (&mut Scoping, &AstNodes<'a>) {
141 (&mut self.scoping, &self.nodes)
142 }
143
144 pub fn classes(&self) -> &ClassTable<'_> {
146 &self.classes
147 }
148
149 pub fn set_irregular_whitespaces(&mut self, irregular_whitespaces: Box<[Span]>) {
151 self.irregular_whitespaces = irregular_whitespaces;
152 }
153
154 pub fn comments(&self) -> &[Comment] {
156 self.comments
157 }
158
159 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 pub fn has_comments_between(&self, span: Span) -> bool {
169 has_comments_between(self.comments, span)
170 }
171
172 pub fn is_inside_comment(&self, pos: u32) -> bool {
174 is_inside_comment(self.comments, pos)
175 }
176
177 pub fn get_comment_at(&self, pos: u32) -> Option<&Comment> {
179 get_comment_at(self.comments, pos)
180 }
181
182 pub fn irregular_whitespaces(&self) -> &[Span] {
184 &self.irregular_whitespaces
185 }
186
187 #[cfg(feature = "jsdoc")]
191 pub fn jsdoc(&self) -> &JSDocFinder<'a> {
192 &self.jsdoc
193 }
194
195 pub fn unused_labels(&self) -> &Vec<NodeId> {
197 &self.unused_labels
198 }
199
200 #[cfg(feature = "cfg")]
205 pub fn cfg(&self) -> Option<&ControlFlowGraph> {
206 self.cfg.as_ref()
207 }
208
209 #[cfg(not(feature = "cfg"))]
213 #[expect(clippy::unused_self)]
214 pub fn cfg(&self) -> Option<&()> {
215 None
216 }
217
218 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 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 pub fn symbol_scope(&self, symbol_id: SymbolId) -> ScopeId {
240 self.scoping.symbol_scope_id(symbol_id)
241 }
242
243 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 pub fn symbol_declaration(&self, symbol_id: SymbolId) -> &AstNode<'a> {
253 self.nodes.get_node(self.scoping.symbol_declaration(symbol_id))
254 }
255
256 pub fn is_reference_to_global_variable(&self, ident: &IdentifierReference) -> bool {
258 self.scoping.root_unresolved_references().contains_key(&ident.name)
259 }
260
261 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 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 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 (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 (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 (SourceType::default(), "let a = { b: 1 }; a.b = 2", ReferenceFlags::read()),
386 (SourceType::default(), "let a = { b: 1 }; a.b += 2", ReferenceFlags::read()),
387 (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 (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 (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 (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 (SourceType::default(), "let a, b; b = (0, a++)", ReferenceFlags::read_write()),
409 (
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 (
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 (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 ReferenceFlags::write(),
447 ),
448 (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 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 (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}