Skip to content

Commit 6220cb8

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 1c50b5c commit 6220cb8

File tree

4 files changed

+55
-40
lines changed

4 files changed

+55
-40
lines changed

CHANGELOG.md

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

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

2529
### 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 @@ bexpand = "1.2.0"
1919
fast-glob = "0.4.3"
2020
classification-macros = { path = "../classification-macros" }
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: 51 additions & 11 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::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,44 @@ 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
38+
39+
let starts = TEMPLATE_START_REGEX
40+
.captures_iter(content_as_str)
41+
.collect::<Vec<_>>();
42+
let ends = TEMPLATE_END_REGEX
2943
.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);
44+
.collect::<Vec<_>>();
45+
46+
for start in starts.iter() {
47+
// The language for this block
48+
let lang = start.get(1).unwrap().as_str();
49+
50+
// The HEREDOC delimiter
51+
let delimiter_start = start.get(2).unwrap().as_str();
52+
53+
// Where the "body" starts for the HEREDOC block
54+
let body_start = start.get(0).unwrap().end();
55+
56+
// Look through all of the ends to find a matching language
57+
for end in ends.iter() {
58+
// 1. This must appear after the start
59+
let body_end = end.get(0).unwrap().start();
60+
if body_end < body_start {
61+
continue;
62+
}
63+
64+
// The languages must match otherwise we haven't found the end
65+
let delimiter_end = end.get(1).unwrap().as_str();
66+
if delimiter_end != delimiter_start {
67+
continue;
68+
}
69+
70+
let body = &content_as_str[body_start..body_end];
71+
let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
72+
73+
result.replace_range(body_start..body_end, replaced);
74+
break;
75+
}
3676
}
3777

3878
// Ruby extraction

0 commit comments

Comments
 (0)