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 = "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#[derive(Default)]
67pub struct Semantic<'a> {
68 source_text: &'a str,
70
71 source_type: SourceType,
73
74 nodes: AstNodes<'a>,
76
77 scoping: Scoping,
78
79 classes: ClassTable<'a>,
80
81 comments: &'a [Comment],
83 irregular_whitespaces: Box<[Span]>,
84
85 #[cfg(feature = "linter")]
87 jsdoc: JSDocFinder<'a>,
88
89 unused_labels: Vec<NodeId>,
90
91 #[cfg(feature = "cfg")]
94 cfg: Option<ControlFlowGraph>,
95 #[cfg(not(feature = "cfg"))]
96 #[allow(unused)]
97 cfg: (),
98}
99
100impl<'a> Semantic<'a> {
101 pub fn into_scoping(self) -> Scoping {
103 self.scoping
104 }
105
106 pub fn into_scoping_and_nodes(self) -> (Scoping, AstNodes<'a>) {
108 (self.scoping, self.nodes)
109 }
110
111 pub fn source_text(&self) -> &'a str {
113 self.source_text
114 }
115
116 pub fn source_type(&self) -> &SourceType {
118 &self.source_type
119 }
120
121 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 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 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 #[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 #[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 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 pub fn symbol_scope(&self, symbol_id: SymbolId) -> ScopeId {
222 self.scoping.symbol_scope_id(symbol_id)
223 }
224
225 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 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 (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 (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 (SourceType::default(), "let a = { b: 1 }; a.b = 2", ReferenceFlags::read()),
362 (SourceType::default(), "let a = { b: 1 }; a.b += 2", ReferenceFlags::read()),
363 (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 (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 (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 (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 (SourceType::default(), "let a, b; b = (0, a++)", ReferenceFlags::read_write()),
385 (
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 (
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 (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 ReferenceFlags::write(),
423 ),
424 (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 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 (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}