1#![doc = include_str!("../examples/semantic.rs")]
6use 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};
16pub 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#[derive(Default)]
66pub struct Semantic<'a> {
67 source_text: &'a str,
69
70 source_type: SourceType,
72
73 nodes: AstNodes<'a>,
75
76 scoping: Scoping,
77
78 classes: ClassTable<'a>,
79
80 comments: &'a [Comment],
82 irregular_whitespaces: Box<[Span]>,
83
84 #[cfg(feature = "linter")]
86 jsdoc: JSDocFinder<'a>,
87
88 unused_labels: Vec<NodeId>,
89
90 #[cfg(feature = "cfg")]
93 cfg: Option<ControlFlowGraph>,
94 #[cfg(not(feature = "cfg"))]
95 #[allow(unused)]
96 cfg: (),
97}
98
99impl<'a> Semantic<'a> {
100 pub fn into_scoping(self) -> Scoping {
102 self.scoping
103 }
104
105 pub fn into_scoping_and_nodes(self) -> (Scoping, AstNodes<'a>) {
107 (self.scoping, self.nodes)
108 }
109
110 pub fn source_text(&self) -> &'a str {
112 self.source_text
113 }
114
115 pub fn source_type(&self) -> &SourceType {
117 &self.source_type
118 }
119
120 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 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 #[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 #[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 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 pub fn symbol_scope(&self, symbol_id: SymbolId) -> ScopeId {
212 self.scoping.symbol_scope_id(symbol_id)
213 }
214
215 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 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 (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 (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 (SourceType::default(), "let a = { b: 1 }; a.b = 2", ReferenceFlags::read()),
352 (SourceType::default(), "let a = { b: 1 }; a.b += 2", ReferenceFlags::read()),
353 (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 (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 (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 (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 (SourceType::default(), "let a, b; b = (0, a++)", ReferenceFlags::read_write()),
375 (
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 (
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 (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 ReferenceFlags::write(),
413 ),
414 (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 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 (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}