[ 
https://issues.apache.org/jira/browse/GROOVY-12123?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12123:
-------------------------------
    Description: 
h2. Summary

Hoist effectively-constant regular expressions to synthetic {{private static 
final Pattern}}
fields so they are compiled once, rather than recompiled on every evaluation -- 
most
importantly inside loops. Transparent optimization; no new user-facing syntax.

h2. Motivation

Groovy's {{~/.../}} operator (and the {{=~}} / {{==~}} operators and JDK 
conveniences such
as {{String#matches}}) construct/compile a {{Pattern}} each time they are 
evaluated. In a
loop, a constant pattern is recompiled every iteration:

{code:groovy}
for (line in lines) {
    if (line ==~ /\d{4}-\d{2}-\d{2}/) { ... } // recompiled every iteration 
today
}
{code}

{{Pattern.compile}} is not cheap, and the pattern here never changes. Lifting 
it to a
compile-once {{static final}} field removes the repeated compilation with no 
change in
observable behaviour.

h2. Proposal

Where the compiler can prove a pattern is *effectively constant*, replace the 
inline
regex expression with a reference to a synthetic {{private static final 
Pattern}} field on
the enclosing class, initialized once. Identical (source + flags) patterns in 
the same
class share a single field.

{code:groovy}
// conceptually, the loop above becomes:
private static final Pattern $pat$0 = Pattern.compile('\\d{4}-\\d{2}-\\d{2}')
...
for (line in lines) {
    if ($pat$0.matcher(line).matches()) { ... }
}
{code}

This is loop-invariant code motion specialized for regex construction. It is 
applied
automatically by the compiler when safe (see _Applicability_ and 
_Correctness_), not via
an annotation.

h2. Applicability ("was applicable")

Only patterns with no runtime-dependent parts qualify:

* {{~/literal/}} with no interpolation -- hoistable.
* {{Pattern.compile("literal"[, constantFlags])}} -- hoistable (method-call 
form).
* {{~"text ${CONST}"}} where the interpolated hole constant-folds -- hoistable 
after folding.
* {{~"text ${var}"}} with a runtime {{var}} -- *not* hoistable; must build each 
time.
* Any pattern closing over a captured, non-constant local -- *not* hoistable.

The primary targets are the {{~/.../}} operator and {{Pattern.compile}}. 
Rewriting the JDK
convenience methods ({{String#matches}}/{{replaceAll}}/{{split}} with a 
constant arg) into a
hoisted-{{Pattern}} form is a larger semantic change (routing through 
{{Pattern}}/{{Matcher}})
and is a possible follow-up extension, not the initial cut.

h2. Correctness / caveats

* *Thread-safety is fine.* {{Pattern}} is immutable and thread-safe, so a shared
  {{static final}} field is safe. {{Matcher}} is stateful and is *never* 
hoisted -- only the
  {{Pattern}}, with a fresh {{Matcher}} per match as today.
* *Exception timing.* An *invalid* constant pattern currently throws 
{{PatternSyntaxException}}
  at the point of evaluation (possibly never, if guarded by a branch). Eager 
static-field
  init would move that to class initialization 
({{ExceptionInInitializerError}}). Mitigation:
  only hoist patterns already known valid (the {{RegexChecker}} type checker 
validates constant
  literals), and/or use a lazy holder so init timing is preserved.
* *Identity.* User code must not rely on each {{~/.../}} yielding a distinct 
{{Pattern}}
  instance ({{===}}); {{Pattern}} carries no meaningful identity or mutability 
contract, so
  sharing is observationally equivalent.

h2. Scope

*In scope*
* Hoisting constant {{~/.../}} and {{Pattern.compile(constant[, constFlags])}} 
to synthetic
  {{static final}} fields, with per-class deduplication of identical patterns.
* Applying inside loops, closures, and script bodies (field on the enclosing 
class).

*Out of scope / deferred*
* Rewriting {{String#matches}}/{{replaceAll}}/{{split}} convenience methods 
(follow-up).
* Hoisting non-constant patterns or any form of runtime {{Pattern}} cache.
* Cross-class / cross-method caching.

h2. Relates to / must compose with GROOVY-12122

GROOVY-12122 adds a ReDoS runtime guard ({{RegexGuard}} helper, {{@SafeRegex}} 
scoped
annotation). The two touch the same regex AST surface and sit on opposite ends 
of the same
operation -- this ticket optimizes *Pattern construction*, GROOVY-12122 guards 
*matching* --
so they compose, but the transforms must be ordering-aware:

* When {{@SafeRegex}} rewrites {{input ==~ /(a+)+$/}} to
  {{RegexGuard.matches(~/(a+)+$/, input, ...)}}, this optimization must still 
recognize the
  {{~/(a+)+$/}} argument as a hoistable constant and lift it.
* When this optimization runs first and produces a {{static final Pattern P}}, 
{{@SafeRegex}}
  must still recognize a match against {{P}} and wrap the input.

Desired combined result: the constant {{Pattern}} is compiled once into a 
{{static final}}
field *and* each match wraps the input in the deadline guard. Whoever 
implements the second
of the two should account for the first.

h2. Implementation notes

* A compiler optimization pass / AST transform over resolved expressions; runs 
in both
  dynamic and {{@CompileStatic}} modes.
* Constant detection reuses the same effectively-constant analysis as constant 
folding.
* Synthetic field naming should be collision-free and deduplicated per class.
* Consider gating behind a flag initially (or restricting to 
{{RegexChecker}}-validated
  patterns) until the exception-timing behaviour is settled.

h2. References

* Related: GROOVY-12122 (ReDoS runtime guard -- RegexGuard / @SafeRegex): 
https://issues.apache.org/jira/browse/GROOVY-12122
* {{java.util.regex.Pattern}} -- immutable/thread-safe; {{Matcher}} -- not 
thread-safe
* {{RegexChecker}} in groovy-typecheckers (compile-time constant-pattern 
validation)

  was:
h2. Summary

Add an opt-in, thread-free runtime guard against Regular Expression Denial of 
Service
(ReDoS / catastrophic backtracking) for Groovy's regex features, offered as a 
runtime
helper ({{RegexGuard}}), a scoped annotation ({{@SafeRegex}}), and type-checker 
awareness
of both. Everything is opt-in; no existing code changes behaviour.

h2. Motivation

Groovy makes regex frictionless -- {{=~}}, {{==~}}, {{~/.../}}, 
{{String#matches}},
{{replaceAll}}, {{split}}, and the {{Matcher}} sugar -- which also makes ReDoS 
easy to
reach when a pattern (or its input) comes from outside the program: scripts, 
build files,
web handlers, {{ConfigSlurper}}, templating.

{{java.util.regex}} has *no native timeout*, so a crafted input against a
backtracking-prone pattern can hang the calling thread indefinitely. Nothing in 
Groovy
guards against this today. Note the trust boundary is usually the *input*, not 
the
pattern: the textbook ReDoS applies a hardcoded, developer-written validator to
attacker-supplied text, so "don't run untrusted code" is not a sufficient 
answer.

Thread interruption does not help: the regex engine never checks the interrupt 
flag, so
neither {{@ThreadInterrupt}} nor {{@TimedInterrupt}} can stop a runaway match 
-- the hang
is inside a single JDK library call, not in user code where those transforms 
insert checks.

JLine 4.3.1 shipped a {{SafeRegex}} utility using the well-known 
{{TimeoutCharSequence}}
technique to close four CVEs (GHSA-r2xf-8xr9-62gw, GHSA-2v9w-34q6-wpqx,
GHSA-ph9c-7hw9-vhhw, GHSA-5q95-hrpc-m3w3). We apply the same idea.

h2. Proposal

Bound regex matching by a wall-clock deadline, without a watchdog thread. The 
input
{{CharSequence}} is wrapped so that {{charAt()}} checks a deadline and throws 
an unchecked
{{RegexTimeoutException}} once it has passed; because the regex engine re-reads 
input
characters continuously while backtracking, a runaway match is interrupted with 
no extra
thread and no {{Thread.interrupt}} plumbing. The clock is consulted once every 
512 reads,
so evaluations finishing in fewer reads never pay for a clock call.

Four ways to apply it, from most explicit to most ergonomic.

*(a) Explicit guarded evaluation* -- guarded equivalents of the match and find 
operators,
with a {{long}} millisecond or {{Duration}} timeout:

{code:groovy}
RegexGuard.matches(pattern, input, 200)                        // -> boolean
RegexGuard.matcher(pattern, input, Duration.ofMillis(200))     // -> Matcher 
over guarded input
{code}

*(b) Guarded data* -- attach a single total deadline to the sequence itself. 
Wrap
untrusted input once at a trust boundary and pass it around freely; every regex 
evaluated
against it shares the one budget, including Groovy's {{CharSequence}} extension 
methods
and anything handing the sequence to {{java.util.regex}} untouched:

{code:groovy}
def safe = RegexGuard.guard(untrusted, 200)
safe.findAll(~/\d+/)
{code}

*(c) Ambient scope* -- a per-evaluation timeout for the dynamic extent of a 
closure on the
current thread, covering the regex operators and the {{CharSequence}} extension 
methods,
including in methods called from the closure:

{code:groovy}
def groups = RegexGuard.guard(200) {
    input.findGroups(/(\d+)\.(\d+)\.(\d+)/)
}
{code}

Nested scopes only tighten -- the smaller per-evaluation timeout wins -- and an 
explicit
timeout from (a) is likewise capped by an active ambient timeout.

*(d) Scoped annotation ({{@SafeRegex}})* -- an AST transform rewrites the match 
({{==~}})
and find ({{=~}}) operators lexically within the annotated scope to guarded 
calls, so no
call-site changes are needed. Fits the existing {{@TimedInterrupt}} / 
{{@ThreadInterrupt}}
/ {{@ConditionalInterrupt}} family in {{groovy.transform}}:

{code:groovy}
@SafeRegex(millis = 200)
class Handler {
    boolean check(String input) {
        input ==~ /(a+)+$/   // rewritten -> RegexGuard.matchRegex(input, 
/(a+)+$/, 200)
    }
}
{code}

The annotation may be placed on a class, method, constructor, field or local 
variable
declaration; on a field or local variable only the initializer expression is 
guarded. Its
single attribute is {{millis}} (default {{1000}}, must be a positive constant). 
A guarded
field initializer runs, as usual, in the constructor -- or in the static 
initializer for a
static field, where a timeout surfaces as {{ExceptionInInitializerError}} whose 
cause is
the {{RegexTimeoutException}}.

The transform targets the operator-order entry points {{matchRegex(input, 
pattern,
millis)}} and {{findRegex(input, pattern, millis)}} rather than
{{matches(pattern, input, millis)}}, so a rewritten expression still evaluates 
its
operands left to right exactly as the original operator did.

The guard is on *matching*, not on {{~/.../}} compilation: {{Pattern.compile}} 
is not
subject to backtracking and is not guarded.

h3. Naming

|| Concern || Name || Notes ||
| Scoped annotation | {{@SafeRegex}} | in {{groovy.transform}}, alongside 
{{@TimedInterrupt}} et al. |
| Runtime helper | {{RegexGuard}} | in {{groovy.util.regex}}; distinct from the 
annotation to avoid a clash |
| Timeout exception | {{RegexTimeoutException}} | unchecked; lets callers 
distinguish a ReDoS abort from a normal non-match |
| Operator-order entry points | {{matchRegex}} / {{findRegex}} | mirror 
{{ScriptBytecodeAdapter}} naming and operand order; used by the transform |

Both {{RegexGuard}} and {{@SafeRegex}} are marked {{@Incubating}}, {{@since 
6.0.0}}.

h3. Implementation notes

* The ambient scope in (c) is held in an 
{{org.apache.groovy.runtime.async.ScopedLocal}},
  which delegates to {{java.lang.ScopedValue}} on JDK 25+ and falls back to a
  save-and-restore {{ThreadLocal}} below, so the binding cannot outlive its 
closure.
* A process that never installs an ambient guard never touches that scoped 
local: a
  monotonic static flag short-circuits the lookup, leaving a single cached read 
on the
  regex path.
* Deadlines are only ever compared as {{System.nanoTime()}} differences, which 
stays
  correct across wrap-around; a timeout large enough to overflow the deadline 
therefore
  reads as "effectively no timeout" rather than expiring immediately.

h2. Scope

*In scope*
* Runtime deadline guard ({{TimeoutCharSequence}}-style), thread-free
* {{RegexGuard}} helper: explicit guarded evaluation, guarded data, and ambient 
closure scope
* {{@SafeRegex}} scoped annotation rewriting the {{==~}} and {{=~}} operators
* {{RegexTimeoutException}} type
* {{RegexChecker}} (groovy-typecheckers) awareness of guarded evaluation, so 
code inside a
  {{@SafeRegex}} scope keeps full regex checking after the rewrite
* Documentation in the operator, metaprogramming and type-checker guides

*Out of scope / deferred*
* *Internal hardening of Groovy's own regex call sites* ({{groovysh}} 
completion,
  templating, builders) -- proposed originally, not implemented here; deferred 
to a
  follow-up so this ticket stays a purely opt-in, additive change
* Making the guard the default for {{=~}} / {{==~}} (behaviour change + 
overhead)
* Rewriting the {{String#matches}} / {{replaceAll}} / {{split}} family of 
method calls
  within a {{@SafeRegex}} scope -- only the two operators are rewritten; use
  {{RegexGuard}} explicitly for the rest (see _Possible future work_ below)
* Static ReDoS pattern detection -- a possible future {{RegexChecker}} extension
* Guarding {{Pattern.compile}}
* Rewriting the regex engine or adopting a non-backtracking (RE2-style) engine

h2. Limitations

* {{@SafeRegex}} rewrites only the *lexically visible* {{==~}} and {{=~}} 
operators --
  scoped hardening, not whole-program. Regex evaluation via method calls, or 
occurring in
  code called from the annotated scope, is not rewritten; use {{RegexGuard}} 
there, whose
  ambient and guarded-data forms do reach both.
* Direct JDK calls bypass the guard. On a {{String}} receiver, 
{{matches(String)}},
  {{replaceAll(String, String)}}, {{replaceFirst(String, String)}} and 
{{split(String)}}
  dispatch to {{java.lang.String}} and never enter Groovy's runtime. Their
  {{Pattern}}-argument variants are extension methods and are covered.
* The deadline fires only while the engine is reading input characters (the 
backtracking
  phase) -- true of catastrophic cases, but not a hard guarantee for every 
pathological
  state. A well-behaved evaluation over a short input may still succeed after 
the deadline.
* The clock is consulted every 512 character reads rather than on every read, 
so overshoot
  past the deadline is bounded by those reads; pathological patterns perform 
millions of
  reads per second, so in practice the overshoot is negligible.
* The ambient scope is thread-confined: threads spawned within the closure are 
not covered
  (on JDK 25+ the binding does propagate into {{StructuredTaskScope}} forks).
* Reduces hang risk; it is not a correctness or a data-at-rest control.

h2. Relationship to existing/planned features

|| Layer || Mechanism || Status || Covers ReDoS? ||
| Compile-time validity | {{RegexChecker}} (groovy-typecheckers) | ships | No 
-- pattern *syntax* only |
| Compile-time ReDoS lint | possible {{RegexChecker}} extension | idea | 
Partially |
| Untrusted-input tracking | GEP-25 {{@Tainted}} -> regex sink | draft | 
Identifies risky matches |
| *Runtime guard (this ticket)* | {{RegexGuard}} / {{@SafeRegex}} | 
*implemented* | *Yes -- the actual backstop* |

{{RegexChecker}} only validates that a literal pattern compiles; it says 
nothing about
backtracking. As part of this ticket it was extended to recognise the guarded 
forms, so
the rewrite performed by {{@SafeRegex}} does not blind it: bad regex literals 
are still
reported, and matcher group-count inference survives the rewrite.

h2. Interaction with GROOVY-12123 (Groovy 7)

This ticket targets Groovy 6. GROOVY-12123 (constant regex hoisting) targets 
Groovy 7, so
it lands second and owns the interaction: by then the guarded forms above are 
already
shipped and in use. Within a single compilation the two still touch the same 
expressions,
so the hoisting pass must be aware of the compile-phase ordering:

* If hoisting runs after {{@SafeRegex}} (which rewrites at 
{{SEMANTIC_ANALYSIS}}), it sees
  {{RegexGuard.matchRegex(input, /(a+)+$/, 200)}} instead of a bare operator. 
The pattern
  is the *second* argument of {{matchRegex}} / {{findRegex}} -- and the *first* 
of a
  hand-written {{matches}} / {{matcher}} -- and must still be recognised as a 
hoistable
  constant and lifted to a {{static final}} field.
* If hoisting runs first and produces a {{static final Pattern P}}, nothing 
further is
  needed: {{@SafeRegex}} rewrites a {{==~}} / {{=~}} expression whatever its 
right operand
  is, and {{RegexGuard}} accepts a {{Pattern}} directly, so {{input ==~ P}} 
already guards
  correctly today.

Desired combined result: the constant {{Pattern}} is compiled once into a 
{{static final}}
field *and* each match wraps the input in the deadline guard.

h2. Possible future work

*Guarding regex method calls within a {{@SafeRegex}} scope.* The transform 
rewrites only
the {{==~}} and {{=~}} operators, so a call such as 
{{input.replaceAll(/(a+)+$/, 'x')}}
inside an annotated scope stays unguarded. This deserves its own ticket rather 
than a
documentation footnote, because for the most common shape -- a {{String}} 
receiver with
{{String}} arguments -- a compile-time rewrite is the *only* mechanism that can 
work:
{{matches(String)}}, {{replaceAll(String, String)}}, {{replaceFirst(String, 
String)}} and
{{split(String)}} all dispatch to {{java.lang.String}} and never enter Groovy's 
runtime, so
no runtime hook, the ambient guard included, can ever observe them. Their
{{Pattern}}-argument variants are extension methods and are already covered.

Such a rewrite would have to be receiver-aware, since these names exist on 
unrelated types
({{List#replaceAll(UnaryOperator)}}, Groovy's own 
{{Collection#split(Closure)}}) and the
receiver type is generally unknown at {{SEMANTIC_ANALYSIS}}. Two workable 
routes:

* redirect to a runtime-dispatching helper that guards a {{CharSequence}} 
receiver and
  otherwise forwards the call unchanged, preserving dynamic semantics; or
* restrict the rewrite to statically-typed code, where the receiver type is 
known.

*Whole-body guarding as an additional {{@SafeRegex}} strategy.* Instead of 
rewriting
individual expressions, the transform could wrap an annotated body in the 
ambient guard
(form (c) above). That would cover everything reachable dynamically on the 
calling thread
-- the operators, the {{CharSequence}} extension methods, and regex in *called* 
methods,
which a lexical rewrite can never reach.

The two strategies are complementary rather than one dominating the other, so 
this would
be an opt-in mode alongside the current rewrite, not a replacement:

* Dynamic scope covers called code; lexical scope does not.
* Lexical scope covers a closure defined in the annotated scope but invoked 
later, or on
  another thread, because the rewrite is baked into the closure's body. Dynamic 
scope
  would not, since the binding unwinds when the annotated method returns.
* Dynamic scope costs a scoped binding per invocation, and what is guarded then 
depends on
  the call graph rather than on what is visible in the source, which is harder 
to audit.
* Neither closes the {{java.lang.String}} dispatch gap described above.

h2. Licensing / reuse

JLine is BSD-3 (ASF category A) and already on the {{groovysh}} classpath
(jline-builtins/jansi), so {{groovysh}} may use JLine's {{SafeRegex}} directly. 
For *core*
Groovy the technique is ~20 lines and well-known, so it is reimplemented from 
the
technique rather than taking a core dependency on JLine. No code was copied.

h2. References

* JLine 4.3.1 release notes -- SafeRegex / TimeoutCharSequence: 
https://github.com/jline/jline3/releases/tag/4.3.1
* CWE-1333: Inefficient Regular Expression Complexity: 
https://cwe.mitre.org/data/definitions/1333.html
* OWASP -- Regular expression Denial of Service (ReDoS): 
https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
* Related: GEP-25 (Data-flow Security -- taint tracking); {{RegexChecker}} in 
groovy-typecheckers; {{@TimedInterrupt}} / {{@ThreadInterrupt}} / 
{{@ConditionalInterrupt}} in groovy.transform
* Relates to: GROOVY-12123 (constant regex hoisting -- compile-once static 
final Pattern fields): https://issues.apache.org/jira/browse/GROOVY-12123
* Pull request: https://github.com/apache/groovy/pull/2668



> Hoist constant regexes to static final fields
> ---------------------------------------------
>
>                 Key: GROOVY-12123
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12123
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>             Fix For: 7.x
>
>
> h2. Summary
> Hoist effectively-constant regular expressions to synthetic {{private static 
> final Pattern}}
> fields so they are compiled once, rather than recompiled on every evaluation 
> -- most
> importantly inside loops. Transparent optimization; no new user-facing syntax.
> h2. Motivation
> Groovy's {{~/.../}} operator (and the {{=~}} / {{==~}} operators and JDK 
> conveniences such
> as {{String#matches}}) construct/compile a {{Pattern}} each time they are 
> evaluated. In a
> loop, a constant pattern is recompiled every iteration:
> {code:groovy}
> for (line in lines) {
>     if (line ==~ /\d{4}-\d{2}-\d{2}/) { ... } // recompiled every iteration 
> today
> }
> {code}
> {{Pattern.compile}} is not cheap, and the pattern here never changes. Lifting 
> it to a
> compile-once {{static final}} field removes the repeated compilation with no 
> change in
> observable behaviour.
> h2. Proposal
> Where the compiler can prove a pattern is *effectively constant*, replace the 
> inline
> regex expression with a reference to a synthetic {{private static final 
> Pattern}} field on
> the enclosing class, initialized once. Identical (source + flags) patterns in 
> the same
> class share a single field.
> {code:groovy}
> // conceptually, the loop above becomes:
> private static final Pattern $pat$0 = Pattern.compile('\\d{4}-\\d{2}-\\d{2}')
> ...
> for (line in lines) {
>     if ($pat$0.matcher(line).matches()) { ... }
> }
> {code}
> This is loop-invariant code motion specialized for regex construction. It is 
> applied
> automatically by the compiler when safe (see _Applicability_ and 
> _Correctness_), not via
> an annotation.
> h2. Applicability ("was applicable")
> Only patterns with no runtime-dependent parts qualify:
> * {{~/literal/}} with no interpolation -- hoistable.
> * {{Pattern.compile("literal"[, constantFlags])}} -- hoistable (method-call 
> form).
> * {{~"text ${CONST}"}} where the interpolated hole constant-folds -- 
> hoistable after folding.
> * {{~"text ${var}"}} with a runtime {{var}} -- *not* hoistable; must build 
> each time.
> * Any pattern closing over a captured, non-constant local -- *not* hoistable.
> The primary targets are the {{~/.../}} operator and {{Pattern.compile}}. 
> Rewriting the JDK
> convenience methods ({{String#matches}}/{{replaceAll}}/{{split}} with a 
> constant arg) into a
> hoisted-{{Pattern}} form is a larger semantic change (routing through 
> {{Pattern}}/{{Matcher}})
> and is a possible follow-up extension, not the initial cut.
> h2. Correctness / caveats
> * *Thread-safety is fine.* {{Pattern}} is immutable and thread-safe, so a 
> shared
>   {{static final}} field is safe. {{Matcher}} is stateful and is *never* 
> hoisted -- only the
>   {{Pattern}}, with a fresh {{Matcher}} per match as today.
> * *Exception timing.* An *invalid* constant pattern currently throws 
> {{PatternSyntaxException}}
>   at the point of evaluation (possibly never, if guarded by a branch). Eager 
> static-field
>   init would move that to class initialization 
> ({{ExceptionInInitializerError}}). Mitigation:
>   only hoist patterns already known valid (the {{RegexChecker}} type checker 
> validates constant
>   literals), and/or use a lazy holder so init timing is preserved.
> * *Identity.* User code must not rely on each {{~/.../}} yielding a distinct 
> {{Pattern}}
>   instance ({{===}}); {{Pattern}} carries no meaningful identity or 
> mutability contract, so
>   sharing is observationally equivalent.
> h2. Scope
> *In scope*
> * Hoisting constant {{~/.../}} and {{Pattern.compile(constant[, 
> constFlags])}} to synthetic
>   {{static final}} fields, with per-class deduplication of identical patterns.
> * Applying inside loops, closures, and script bodies (field on the enclosing 
> class).
> *Out of scope / deferred*
> * Rewriting {{String#matches}}/{{replaceAll}}/{{split}} convenience methods 
> (follow-up).
> * Hoisting non-constant patterns or any form of runtime {{Pattern}} cache.
> * Cross-class / cross-method caching.
> h2. Relates to / must compose with GROOVY-12122
> GROOVY-12122 adds a ReDoS runtime guard ({{RegexGuard}} helper, 
> {{@SafeRegex}} scoped
> annotation). The two touch the same regex AST surface and sit on opposite 
> ends of the same
> operation -- this ticket optimizes *Pattern construction*, GROOVY-12122 
> guards *matching* --
> so they compose, but the transforms must be ordering-aware:
> * When {{@SafeRegex}} rewrites {{input ==~ /(a+)+$/}} to
>   {{RegexGuard.matches(~/(a+)+$/, input, ...)}}, this optimization must still 
> recognize the
>   {{~/(a+)+$/}} argument as a hoistable constant and lift it.
> * When this optimization runs first and produces a {{static final Pattern 
> P}}, {{@SafeRegex}}
>   must still recognize a match against {{P}} and wrap the input.
> Desired combined result: the constant {{Pattern}} is compiled once into a 
> {{static final}}
> field *and* each match wraps the input in the deadline guard. Whoever 
> implements the second
> of the two should account for the first.
> h2. Implementation notes
> * A compiler optimization pass / AST transform over resolved expressions; 
> runs in both
>   dynamic and {{@CompileStatic}} modes.
> * Constant detection reuses the same effectively-constant analysis as 
> constant folding.
> * Synthetic field naming should be collision-free and deduplicated per class.
> * Consider gating behind a flag initially (or restricting to 
> {{RegexChecker}}-validated
>   patterns) until the exception-timing behaviour is settled.
> h2. References
> * Related: GROOVY-12122 (ReDoS runtime guard -- RegexGuard / @SafeRegex): 
> https://issues.apache.org/jira/browse/GROOVY-12122
> * {{java.util.regex.Pattern}} -- immutable/thread-safe; {{Matcher}} -- not 
> thread-safe
> * {{RegexChecker}} in groovy-typecheckers (compile-time constant-pattern 
> validation)



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to