Skip to content

Commit eee1190

Browse files
Fix Ruby files causing the CLI to hang (#17383)
Fixes #17379 The preprocessor we added to detect embedded languages uses a back reference and given a long enough file with certain byte / character patterns it'll cause what appears to be an indefinite hang (might just be catastrophically exponential backtracking but not sure) This replaces the one regex w/ back references with two, anchored, multi-line regexes Now we search for all the starting & ending delimiters in the file. We then loop over all the starting delimiters, find the paired ending one, and preprocess the content inside --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
1 parent 9b7fdc3 commit eee1190

File tree

4 files changed

+52
-41
lines changed

4 files changed

+52
-41
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
2222
- _Experimental_: Add `@source not` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
2323

24+
### Fixed
25+
26+
- Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383))
27+
2428
## [4.0.16] - 2025-03-25
2529

2630
### Added

Cargo.lock

Lines changed: 0 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxide/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ fast-glob = "0.4.3"
1919
classification-macros = { path = "../classification-macros" }
2020
ignore = { path = "../ignore" }
2121
regex = "1.11.1"
22-
fancy-regex = "0.14.0"
2322

2423
[dev-dependencies]
2524
tempfile = "3.13.0"
26-

crates/oxide/src/extractor/pre_processors/ruby.rs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ use crate::cursor;
44
use crate::extractor::bracket_stack;
55
use crate::extractor::pre_processors::pre_processor::PreProcessor;
66
use crate::scanner::pre_process_input;
7-
use bstr::ByteSlice;
8-
use fancy_regex::Regex;
7+
use bstr::ByteVec;
8+
use regex::{Regex, RegexBuilder};
99
use std::sync;
1010

11-
static TEMPLATE_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
12-
Regex::new(r#"\s*(.*?)_template\s*<<[-~]?([A-Z]+?)\n([\s\S]*?)\2"#).unwrap()
11+
static TEMPLATE_START_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
12+
RegexBuilder::new(r#"\s*([a-z0-9_-]+)_template\s*<<[-~]?([A-Z]+)$"#)
13+
.multi_line(true)
14+
.build()
15+
.unwrap()
16+
});
17+
18+
static TEMPLATE_END_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
19+
RegexBuilder::new(r#"^\s*([A-Z]+)"#)
20+
.multi_line(true)
21+
.build()
22+
.unwrap()
1323
});
1424

1525
#[derive(Debug, Default)]
@@ -25,14 +35,40 @@ impl PreProcessor for Ruby {
2535
// Extract embedded template languages
2636
// https://viewcomponent.org/guide/templates.html#interpolations
2737
let content_as_str = std::str::from_utf8(content).unwrap();
28-
for capture in TEMPLATE_REGEX
29-
.captures_iter(content_as_str)
30-
.filter_map(Result::ok)
31-
{
32-
let lang = capture.get(1).unwrap().as_str();
33-
let body = capture.get(3).unwrap().as_str();
34-
let replaced = pre_process_input(body.as_bytes(), lang);
35-
result = result.replace(body, replaced);
38+
39+
let starts = TEMPLATE_START_REGEX.captures_iter(content_as_str).collect::<Vec<_>>();
40+
let ends = TEMPLATE_END_REGEX.captures_iter(content_as_str).collect::<Vec<_>>();
41+
42+
for start in starts.iter() {
43+
// The language for this block
44+
let lang = start.get(1).unwrap().as_str();
45+
46+
// The HEREDOC delimiter
47+
let delimiter_start = start.get(2).unwrap().as_str();
48+
49+
// Where the "body" starts for the HEREDOC block
50+
let body_start = start.get(0).unwrap().end();
51+
52+
// Look through all of the ends to find a matching language
53+
for end in ends.iter() {
54+
// 1. This must appear after the start
55+
let body_end = end.get(0).unwrap().start();
56+
if body_end < body_start {
57+
continue;
58+
}
59+
60+
// The languages must match otherwise we haven't found the end
61+
let delimiter_end = end.get(1).unwrap().as_str();
62+
if delimiter_end != delimiter_start {
63+
continue;
64+
}
65+
66+
let body = &content_as_str[body_start..body_end];
67+
let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
68+
69+
result.replace_range(body_start..body_end, replaced);
70+
break;
71+
}
3672
}
3773

3874
// Ruby extraction

0 commit comments

Comments
 (0)