1use std::path::Path;
22
23use fallow_types::discover::FileId;
24use fallow_types::extract::ModuleInfo;
25
26use crate::visitor::ModuleInfoExtractor;
27
28pub const MAX_NESTING_DEPTH: u32 = 1024;
35
36pub fn too_deep_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
40 let source = crate::strip_bom(source);
41 let mut info = ModuleInfoExtractor::new().into_module_info(
42 file_id,
43 content_hash,
44 crate::suppress::ParsedSuppressions::default(),
45 );
46 info.parse_error_count = 1;
47 info.parse_panicked = true;
48 info.parse_nesting_too_deep = true;
49 info.line_offsets = fallow_types::extract::compute_line_offsets(source);
50 info
51}
52
53#[must_use]
56pub fn exceeds_nesting_limit(source: &str) -> bool {
57 max_depth(source, MAX_NESTING_DEPTH, Dialect::Script) > MAX_NESTING_DEPTH
58}
59
60#[derive(Clone, Copy, PartialEq, Eq)]
62enum Dialect {
63 Script,
65 Style,
70}
71
72pub fn parse_bounded<R: Send>(source: &str, parse: impl FnOnce() -> R + Send) -> Option<R> {
83 parse_bounded_as(source, Dialect::Script, parse)
84}
85
86pub fn parse_style_bounded<R: Send>(source: &str, parse: impl FnOnce() -> R + Send) -> Option<R> {
89 parse_bounded_as(source, Dialect::Style, parse)
90}
91
92pub fn parse_file_bounded<R: Send>(
95 path: &Path,
96 source: &str,
97 parse: impl FnOnce() -> R + Send,
98) -> Option<R> {
99 let dialect = if crate::css::is_css_file(path) {
100 Dialect::Style
101 } else {
102 Dialect::Script
103 };
104 parse_bounded_as(source, dialect, parse)
105}
106
107fn parse_bounded_as<R: Send>(
108 source: &str,
109 dialect: Dialect,
110 parse: impl FnOnce() -> R + Send,
111) -> Option<R> {
112 let depth = max_depth(source, MAX_NESTING_DEPTH, dialect);
113 if depth > MAX_NESTING_DEPTH {
114 return None;
115 }
116 let need = crate::stack::stack_need(source.len(), depth as usize)?;
117 Some(crate::stack::run_with_stack(need, parse))
118}
119
120pub fn parse_bounded_or_default<T: Send + Default>(
124 source: &str,
125 source_type: oxc_span::SourceType,
126 walk: impl FnOnce(&oxc_parser::ParserReturn<'_>) -> T + Send,
127) -> T {
128 parse_bounded(source, || {
129 let allocator = oxc_allocator::Allocator::default();
130 let parsed = oxc_parser::Parser::new(&allocator, source, source_type).parse();
131 walk(&parsed)
132 })
133 .unwrap_or_default()
134}
135
136fn max_depth(source: &str, limit: u32, dialect: Dialect) -> u32 {
139 let mut scanner = Scanner {
140 bytes: source.as_bytes(),
141 pos: 0,
142 depth: 0,
143 max_depth: 0,
144 limit,
145 dialect,
146 templates: Vec::new(),
147 regex_allowed: true,
148 };
149 scanner.scan();
150 scanner.max_depth
151}
152
153struct Scanner<'a> {
154 bytes: &'a [u8],
155 pos: usize,
156 depth: u32,
157 max_depth: u32,
158 limit: u32,
159 dialect: Dialect,
160 templates: Vec<u32>,
163 regex_allowed: bool,
166}
167
168impl Scanner<'_> {
169 fn scan(&mut self) {
170 while let Some(&byte) = self.bytes.get(self.pos) {
171 if self.max_depth > self.limit {
172 return;
173 }
174 self.pos += 1;
175 match byte {
176 b'\\' if self.dialect == Dialect::Style => self.skip_escape(),
177 b'(' if self.dialect == Dialect::Style && self.at_unquoted_url() => {
178 self.skip_unquoted_url();
179 }
180 b'(' | b'[' | b'{' => {
181 self.open();
182 self.regex_allowed = true;
183 }
184 b'}' if self.templates.last() == Some(&self.depth) => {
185 self.templates.pop();
186 self.depth -= 1;
187 self.skip_template_text();
188 }
189 b')' | b']' | b'}' => {
190 self.depth = self.depth.saturating_sub(1);
191 self.regex_allowed = byte == b'}';
192 }
193 b'\'' | b'"' => {
194 self.skip_string(byte);
195 self.regex_allowed = false;
196 }
197 b'`' if self.dialect == Dialect::Script => self.skip_template_text(),
198 b'/' if self.dialect == Dialect::Style => self.skip_block_comment(),
199 b'/' => self.skip_slash(),
200 b' ' | b'\t' | b'\n' | b'\r' => {}
201 _ => self.regex_allowed = starts_expression(byte),
202 }
203 }
204 }
205
206 fn open(&mut self) {
207 self.depth += 1;
208 self.max_depth = self.max_depth.max(self.depth);
209 }
210
211 fn skip_slash(&mut self) {
213 match self.bytes.get(self.pos) {
214 Some(b'/') => {
215 while self.pos < self.bytes.len() && !self.at_line_terminator() {
216 self.pos += 1;
217 }
218 }
219 Some(b'*') => self.skip_block_comment(),
220 _ if self.may_start_regex() && self.skip_regex() => {
221 self.regex_allowed = false;
222 }
223 _ => self.regex_allowed = true,
224 }
225 }
226
227 fn at_unquoted_url(&self) -> bool {
230 let open = self.pos - 1;
231 let Some(name) = open.checked_sub(3).map(|start| &self.bytes[start..open]) else {
232 return false;
233 };
234 let starts_word = open
235 .checked_sub(4)
236 .is_none_or(|before| !is_css_name_byte(self.bytes[before]));
237 let body = self.bytes[self.pos..].trim_ascii_start();
238 name.eq_ignore_ascii_case(b"url")
239 && starts_word
240 && !matches!(body.first(), Some(b'\'' | b'"'))
241 }
242
243 fn skip_unquoted_url(&mut self) {
248 self.open();
249 let mut local = 0_u32;
250 let mut peak = 0_u32;
251 while let Some(&byte) = self.bytes.get(self.pos) {
252 self.pos += 1;
253 match byte {
254 b'\\' => self.skip_escape(),
255 b'(' | b'[' | b'{' => {
256 local += 1;
257 peak = peak.max(local);
258 }
259 b')' if local == 0 => break,
260 b')' | b']' | b'}' => local = local.saturating_sub(1),
261 _ => {}
262 }
263 }
264 self.max_depth = self.max_depth.max(self.depth + peak);
265 self.depth = self.depth - 1 + local;
266 }
267
268 fn skip_block_comment(&mut self) {
270 if self.bytes.get(self.pos) != Some(&b'*') {
271 return;
272 }
273 self.pos += 1;
274 while self.pos < self.bytes.len() && !self.bytes[self.pos..].starts_with(b"*/") {
275 self.pos += 1;
276 }
277 self.pos = (self.pos + 2).min(self.bytes.len());
278 }
279
280 fn skip_string(&mut self, quote: u8) {
283 let start = self.pos;
284 while let Some(&byte) = self.bytes.get(self.pos) {
285 self.pos += 1;
286 match byte {
287 b'\\' => self.skip_escape(),
288 b'\n' | b'\r' => break,
291 0x0C if self.dialect == Dialect::Style => break,
292 _ if byte == quote => return,
293 _ => {}
294 }
295 }
296 self.pos = start;
297 }
298
299 fn skip_regex(&mut self) -> bool {
305 let start = self.pos;
306 let mut class_base = None;
309 let mut local = 0_u32;
310 let mut peak = 0_u32;
311 while let Some(&byte) = self.bytes.get(self.pos) {
312 if self.at_line_terminator() {
313 break;
314 }
315 self.pos += 1;
316 match byte {
317 b'\\' if self.at_line_terminator() => break,
319 b'\\' => self.pos += 1,
320 b']' if class_base.is_some() => local = class_base.take().unwrap_or(0),
321 b'(' | b'[' | b'{' => {
322 if byte == b'[' && class_base.is_none() {
323 class_base = Some(local);
324 }
325 local += 1;
326 peak = peak.max(local);
327 }
328 b')' | b']' | b'}' => local = local.saturating_sub(1),
329 b'/' if class_base.is_none() => {
330 self.max_depth = self.max_depth.max(self.depth + peak);
331 self.depth += local;
332 return true;
333 }
334 _ => {}
335 }
336 }
337 self.pos = start;
338 false
339 }
340
341 fn skip_template_text(&mut self) {
344 while let Some(&byte) = self.bytes.get(self.pos) {
345 self.pos += 1;
346 match byte {
347 b'\\' => self.skip_escape(),
348 b'`' => {
349 self.regex_allowed = false;
350 return;
351 }
352 b'$' if self.bytes.get(self.pos) == Some(&b'{') => {
353 self.pos += 1;
354 self.open();
355 self.templates.push(self.depth);
356 self.regex_allowed = true;
357 return;
358 }
359 _ => {}
360 }
361 }
362 }
363}
364
365const fn is_css_name_byte(byte: u8) -> bool {
367 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') || byte >= 0x80
368}
369
370const REGEX_KEYWORDS: &[&[u8]] = &[
372 b"await",
373 b"case",
374 b"delete",
375 b"do",
376 b"else",
377 b"in",
378 b"instanceof",
379 b"new",
380 b"of",
381 b"return",
382 b"throw",
383 b"typeof",
384 b"void",
385 b"yield",
386];
387
388impl Scanner<'_> {
389 fn may_start_regex(&self) -> bool {
392 let jsx_tag = self.pos >= 2 && self.bytes[self.pos - 2] == b'<'
393 || self.bytes.get(self.pos) == Some(&b'>');
394 !jsx_tag && (self.regex_allowed || self.after_keyword())
395 }
396
397 fn at_line_terminator(&self) -> bool {
400 self.line_terminator_len() > 0
401 }
402
403 fn line_terminator_len(&self) -> usize {
406 match self.bytes.get(self.pos..) {
407 Some([b'\r', b'\n', ..]) => 2,
408 Some([b'\n' | b'\r', ..]) => 1,
409 Some([0xE2, 0x80, 0xA8 | 0xA9, ..]) => 3,
410 _ => 0,
411 }
412 }
413
414 fn skip_escape(&mut self) {
418 self.pos += self.line_terminator_len().max(1);
419 }
420
421 fn after_keyword(&self) -> bool {
424 let before = &self.bytes[..self.pos - 1];
425 let before = before.trim_ascii_end();
426 let start = before
427 .iter()
428 .rposition(|byte| !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')))
429 .map_or(0, |index| index + 1);
430 REGEX_KEYWORDS.contains(&&before[start..])
431 }
432}
433
434const fn starts_expression(byte: u8) -> bool {
437 !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'.' | b')' | b']'))
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 fn depth_of(source: &str) -> u32 {
445 max_depth(source, u32::MAX, Dialect::Script)
446 }
447
448 fn style_depth_of(source: &str) -> u32 {
449 max_depth(source, u32::MAX, Dialect::Style)
450 }
451
452 #[test]
453 fn style_sources_skip_strings_and_block_comments_only() {
454 assert_eq!(style_depth_of(".a{content:\"(((\"}/* [[[ */"), 1);
455 assert_eq!(
456 style_depth_of(".b{background:url(//cdn.example.com/a.png)}.c{a:b(c(d))}"),
457 3
458 );
459 assert_eq!(style_depth_of(".d{width:calc(100%/3)}"), 2);
460 assert_eq!(
461 style_depth_of(".e::before{content:'it\\'s'}.f{g:h((i))}"),
462 3
463 );
464 assert_eq!(style_depth_of(".g\\\"{a:b((c))}"), 3);
466 assert_eq!(style_depth_of(".h{a:url(/*x.png)}.i{a:b((c))}"), 3);
467 assert_eq!(style_depth_of(".j{a:url(it's.png)}.k{a:b((c))}"), 3);
468 assert_eq!(style_depth_of(".l{a:URL( \"(((\" )}"), 2);
469 assert_eq!(style_depth_of(".m{a:myurl(((b)))}"), 4);
470 assert_eq!(style_depth_of("#url({{{{"), 5);
473 assert_eq!(style_depth_of(".n{a:\"x\u{c}((((1"), 5);
474 }
475
476 #[test]
477 fn counts_code_brackets_only() {
478 assert_eq!(depth_of("f(a[b{c}])"), 3);
479 assert_eq!(
480 depth_of("'((('; \"[[[\"; `{{{`; /\\(\\(/; // (((\n/* ((( */"),
481 0
482 );
483 assert_eq!(depth_of("`a${b(`c${d}`)}`"), 3);
484 assert_eq!(depth_of("a / (b) / c"), 1);
485 assert_eq!(depth_of("x = /[/(]/ + (1)"), 2);
486 assert_eq!(depth_of("return /[(]/.test(a)"), 2);
487 }
488
489 #[test]
490 fn misread_literals_do_not_hide_brackets() {
491 assert_eq!(depth_of("x = i++ / (((1"), 3);
492 assert_eq!(depth_of("x = i++ / (((1 / 2)))"), 3);
493 assert_eq!(depth_of("<p>it's {{{a}}}</p>"), 3);
494 let lines = "x = i++ / ((1 / 2\n".repeat(500);
495 assert_eq!(depth_of(&lines), 1000);
496 for terminator in ["\r", "\u{2028}", "\u{2029}"] {
497 assert_eq!(depth_of(&format!("// a{terminator}(((1")), 3);
498 assert_eq!(depth_of(&format!("x = i++ / a{terminator}(((1")), 3);
499 }
500 assert_eq!(depth_of("'a\rb' + (((1"), 3);
501 assert_eq!(depth_of("((('a\\\r\n)))' + (((1"), 6);
502 assert_eq!(depth_of("((('a\\\u{2028})))' + (((1"), 6);
503 assert_eq!(depth_of("x = i++ /a\\\n(((1 /"), 3);
504 }
505
506 #[test]
507 fn regex_character_classes_do_not_accumulate() {
508 let regex = format!("x = /{}/;\n", "[a-z(][^)]".repeat(2000));
509 assert_eq!(depth_of(®ex), 2);
510 }
511
512 #[test]
513 fn compact_jsx_closers_keep_the_depth_balanced() {
514 let line = "<ul>{xs.map(i => (<li>{i}</li>))}</ul>\n{xs.map(i => <Item {...i} />)}\n";
515 assert_eq!(depth_of(line), 4);
516 assert_eq!(depth_of(&line.repeat(2000)), 4);
517 }
518
519 #[test]
520 fn depth_at_the_cap_is_allowed() {
521 let at_limit = "[".repeat(MAX_NESTING_DEPTH as usize);
522 assert!(!exceeds_nesting_limit(&at_limit));
523 assert!(exceeds_nesting_limit(&format!("{at_limit}[")));
524 }
525}