ion/
stack.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::fmt::{Display, Formatter, Write as _};
8use std::mem::MaybeUninit;
9use std::ptr::NonNull;
10use std::{fmt, ptr};
11
12use mozjs::conversions::jsstr_to_string;
13use mozjs::jsapi::{
14	BuildStackString, CaptureCurrentStack, JS_StackCapture_AllFrames, JS_StackCapture_MaxFrames, JSObject, JSString,
15	StackFormat,
16};
17use mozjs::rooted;
18#[cfg(feature = "sourcemap")]
19use swc_sourcemap::SourceMap;
20
21use crate::format::{INDENT, NEWLINE};
22use crate::utils::normalise_path;
23use crate::{Context, Object};
24
25/// Represents a location in a source file.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Location {
28	pub file: String,
29	pub lineno: u32,
30	pub column: u32,
31}
32
33/// Represents a single stack record of a [stacktrace](Stack).
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct StackRecord {
36	pub function: Option<String>,
37	pub location: Location,
38}
39
40/// Represents a stacktrace.
41///
42/// Holds a stack object if cretead from an [Object].
43#[derive(Clone, Debug)]
44pub struct Stack {
45	pub records: Vec<StackRecord>,
46	pub object: Option<*mut JSObject>,
47}
48
49impl Location {
50	/// Transforms a [Location], according to the given [SourceMap].
51	#[cfg(feature = "sourcemap")]
52	pub fn transform_with_sourcemap(&mut self, sourcemap: &SourceMap) {
53		if self.lineno != 0
54			&& self.column != 0
55			&& let Some(token) = sourcemap.lookup_token(self.lineno - 1, self.column - 1)
56		{
57			self.lineno = token.get_src_line() + 1;
58			self.column = token.get_src_col() + 1;
59		}
60	}
61}
62
63impl StackRecord {
64	/// Transforms a [StackRecord], according to the given [SourceMap].
65	#[cfg(feature = "sourcemap")]
66	pub fn transform_with_sourcemap(&mut self, sourcemap: &SourceMap) {
67		self.location.transform_with_sourcemap(sourcemap);
68	}
69}
70
71impl Display for StackRecord {
72	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
73		f.write_str(self.function.as_deref().unwrap_or(""))?;
74		f.write_char('@')?;
75		f.write_str(&self.location.file)?;
76		f.write_char(':')?;
77		f.write_str(&self.location.lineno.to_string())?;
78		f.write_char(':')?;
79		f.write_str(&self.location.column.to_string())?;
80		Ok(())
81	}
82}
83
84impl Stack {
85	/// Creates a [Stack] from a string.
86	pub fn from_string(string: &str) -> Stack {
87		let mut records = Vec::new();
88		for line in string.lines() {
89			let (function, line) = line.split_once('@').unwrap();
90			let (line, column) = line.rsplit_once(':').unwrap();
91			let (file, lineno) = line.rsplit_once(':').unwrap();
92
93			let function = if function.is_empty() {
94				None
95			} else {
96				Some(String::from(function))
97			};
98			let file = String::from(normalise_path(file).to_str().unwrap());
99			let lineno = lineno.parse().unwrap();
100			let column = column.parse().unwrap();
101
102			records.push(StackRecord {
103				function,
104				location: Location { file, lineno, column },
105			});
106		}
107		Stack { records, object: None }
108	}
109
110	/// Creates a [Stack] from an object.
111	pub fn from_object(cx: &Context, stack: *mut JSObject) -> Option<Stack> {
112		stack_to_string(cx, stack).as_deref().map(Stack::from_string).map(|mut s| {
113			s.object = Some(stack);
114			s
115		})
116	}
117
118	/// Captures the [Stack] of the [Context].
119	pub fn from_capture(cx: &Context) -> Option<Stack> {
120		let stack = capture_stack(cx, None)?;
121		Stack::from_object(cx, stack)
122	}
123
124	/// Returns `true` if the stack contains no [records](StackRecord)
125	pub fn is_empty(&self) -> bool {
126		self.records.is_empty()
127	}
128
129	/// Transforms a [Stack] with the given [SourceMap], by applying it to each of its [records](StackRecord).
130	#[cfg(feature = "sourcemap")]
131	pub fn transform_with_sourcemap(&mut self, sourcemap: &SourceMap) {
132		for record in &mut self.records {
133			record.transform_with_sourcemap(sourcemap);
134		}
135	}
136
137	/// Formats the [Stack] as a String.
138	pub fn format(&self) -> String {
139		let mut string = String::new();
140		for record in &self.records {
141			string.push_str(INDENT);
142			string.push_str(&record.to_string());
143			string.push_str(NEWLINE);
144		}
145		string.pop();
146		string
147	}
148}
149
150impl Display for Stack {
151	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
152		f.write_str(&self.format())
153	}
154}
155
156fn capture_stack(cx: &Context, max_frames: Option<u32>) -> Option<*mut JSObject> {
157	unsafe {
158		let mut capture = MaybeUninit::uninit();
159		match max_frames {
160			None => JS_StackCapture_AllFrames(capture.as_mut_ptr()),
161			Some(count) => JS_StackCapture_MaxFrames(count, capture.as_mut_ptr()),
162		}
163		let mut capture = capture.assume_init();
164
165		let mut stack = Object::null(cx);
166		let start_at = Object::null(cx);
167		CaptureCurrentStack(
168			cx.as_ptr(),
169			stack.handle_mut().into(),
170			&raw mut capture,
171			start_at.handle().into(),
172		)
173		.then(|| stack.handle().get())
174	}
175}
176
177fn stack_to_string(cx: &Context, stack: *mut JSObject) -> Option<String> {
178	unsafe {
179		rooted!(in(cx.as_ptr()) let stack = stack);
180		rooted!(in(cx.as_ptr()) let mut string: *mut JSString);
181
182		BuildStackString(
183			cx.as_ptr(),
184			ptr::null_mut(),
185			stack.handle().into(),
186			string.handle_mut().into(),
187			0,
188			StackFormat::SpiderMonkey,
189		)
190		.then(|| NonNull::new(string.get()).map(|str| jsstr_to_string(cx.as_ptr(), str)))
191		.flatten()
192	}
193}