Skip to content

Commit 5529450

Browse files
committed
feat: detect infinite recursion
1 parent d00dbcd commit 5529450

File tree

8 files changed

+49
-8
lines changed

8 files changed

+49
-8
lines changed

pomsky-lib/src/diagnose/compile_error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub(crate) enum CompileErrorKind {
9595
flavor: RegexFlavor,
9696
},
9797
NestedTest,
98+
InfiniteRecursion,
9899
BadIntersection,
99100
EmptyIntersection,
100101
}
@@ -219,6 +220,7 @@ impl core::fmt::Display for CompileErrorKind {
219220
),
220221
_ => write!(f, "This kind of lookbehind is not supported in the {flavor:?} flavor"),
221222
},
223+
CompileErrorKind::InfiniteRecursion => write!(f, "This recursion never terminates"),
222224
CompileErrorKind::BadIntersection => write!(
223225
f,
224226
"Intersecting these expressions is not supported. Only character sets \

pomsky-lib/src/diagnose/diagnostic_code.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ diagnostic_code! {
9898
RubyLookaheadInLookbehind = 319,
9999
UnsupportedInLookbehind = 320,
100100
LookbehindNotConstantLength = 321,
101-
BadIntersection = 322,
102-
EmptyIntersection = 323,
101+
InfiniteRecursion = 322,
102+
BadIntersection = 323,
103+
EmptyIntersection = 324,
103104

104105
// Warning indicating something might not be supported
105106
PossiblyUnsupported = 400,
@@ -234,6 +235,7 @@ impl<'a> From<&'a CompileErrorKind> for DiagnosticCode {
234235
C::UnsupportedInLookbehind { .. } => Self::UnsupportedInLookbehind,
235236
C::LookbehindNotConstantLength { .. } => Self::LookbehindNotConstantLength,
236237
C::NestedTest => Self::NestedTest,
238+
C::InfiniteRecursion => Self::InfiniteRecursion,
237239
C::BadIntersection => Self::BadIntersection,
238240
C::EmptyIntersection => Self::EmptyIntersection,
239241
}

pomsky-lib/src/diagnose/diagnostic_kind.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ impl From<&CompileErrorKind> for DiagnosticKind {
4040
| K::NameUsedMultipleTimes(_)
4141
| K::UnknownVariable { .. }
4242
| K::RelativeRefZero => DiagnosticKind::Resolve,
43-
K::EmptyClassNegated { .. } | K::IllegalNegation { .. } | K::EmptyIntersection => {
44-
DiagnosticKind::Invalid
45-
}
43+
K::EmptyClassNegated { .. }
44+
| K::InfiniteRecursion
45+
| K::IllegalNegation { .. }
46+
| K::EmptyIntersection => DiagnosticKind::Invalid,
4647
K::CaptureInLet
4748
| K::ReferenceInLet
4849
| K::RecursiveVariable

pomsky-lib/src/diagnose/help.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ pub(super) fn get_compiler_help(kind: &CompileErrorKind, _span: Span) -> Option<
180180
Parentheses may be required to clarify the parsing order."
181181
.to_string(),
182182
),
183+
CompileErrorKind::InfiniteRecursion => Some(
184+
"A recursive expression must have a branch that \
185+
doesn't reach the `recursion`, or can repeat 0 times"
186+
.to_string(),
187+
),
183188

184189
_ => None,
185190
}

pomsky-lib/src/exprs/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
capturing_groups::CapturingGroupsCollector,
33
compile::{CompileResult, CompileState},
4-
diagnose::Diagnostic,
4+
diagnose::{CompileErrorKind, Diagnostic},
55
options::CompileOptions,
66
regex::Count,
77
validation::Validator,
@@ -60,7 +60,8 @@ impl Expr {
6060
input: &str,
6161
options: CompileOptions,
6262
) -> (Option<String>, Vec<Diagnostic>) {
63-
if let Err(e) = Validator::new(options).visit_rule(&self.0) {
63+
let mut validator = Validator::new(options);
64+
if let Err(e) = validator.visit_rule(&self.0) {
6465
return (None, vec![e.diagnostic(input)]);
6566
}
6667

@@ -90,6 +91,12 @@ impl Expr {
9091
Ok(compiled) => compiled,
9192
Err(e) => return (None, vec![e.diagnostic(input)]),
9293
};
94+
if let Some(rec_span) = validator.first_recursion {
95+
if !compiled.terminates() {
96+
let error = CompileErrorKind::InfiniteRecursion.at(rec_span);
97+
return (None, vec![error.diagnostic(input)]);
98+
}
99+
}
93100
let count = compiled.optimize();
94101

95102
let mut buf = String::new();

pomsky-lib/src/regex/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ impl Regex {
156156
matches!(self, Regex::CharSet(_))
157157
}
158158
}
159+
160+
pub(super) fn terminates(&self) -> bool {
161+
match self {
162+
Regex::Recursion => false,
163+
Regex::Repetition(repetition) => {
164+
repetition.kind.lower_bound == 0 || repetition.content.terminates()
165+
}
166+
Regex::Group(group) => group.parts.iter().all(|part| part.terminates()),
167+
Regex::Alternation(alternation) => alternation.parts.iter().any(|alt| alt.terminates()),
168+
Regex::Lookaround(lookaround) => lookaround.content.terminates(),
169+
_ => true,
170+
}
171+
}
159172
}
160173

161174
impl Default for Regex {

pomsky-lib/src/validation.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ use crate::{
1010
#[derive(Clone)]
1111
pub(crate) struct Validator {
1212
pub(crate) options: CompileOptions,
13+
pub(crate) first_recursion: Option<Span>,
1314
pub(crate) layer: u32,
1415
}
1516

1617
impl Validator {
1718
pub(crate) fn new(options: CompileOptions) -> Self {
18-
Validator { options, layer: 0 }
19+
Validator { options, first_recursion: None, layer: 0 }
1920
}
2021

2122
fn require(&self, feature: u16, span: Span) -> Result<(), CompileError> {
@@ -132,6 +133,10 @@ impl RuleVisitor<CompileError> for Validator {
132133
fn visit_recursion(&mut self, recursion: &exprs::Recursion) -> Result<(), CompileError> {
133134
self.require(Feat::RECURSION, recursion.span)?;
134135

136+
if self.first_recursion.is_none() {
137+
self.first_recursion = Some(recursion.span);
138+
}
139+
135140
if let RegexFlavor::Pcre | RegexFlavor::Ruby = self.flavor() {
136141
Ok(())
137142
} else {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#! expect=error, flavor=Pcre
2+
("foo" recursion)+
3+
-----
4+
ERROR: This recursion never terminates
5+
HELP: A recursive expression must have a branch that doesn't reach the `recursion`, or can repeat 0 times
6+
SPAN: 7..16

0 commit comments

Comments
 (0)