Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0c09aa
Added new gene: AssertionRxGene.
lmasroca Jul 13, 2026
bc2f711
Updated gene count to reflect new gene added.
lmasroca Jul 13, 2026
d46545c
Added sampler for new gene.
lmasroca Jul 13, 2026
06bf520
Lowered chance for large trees.
lmasroca Jul 13, 2026
c8397fd
Added new interface for assertion repair usage.
lmasroca Jul 13, 2026
44f421d
Added external flags to RegexGene.
lmasroca Jul 13, 2026
655c915
Passed external flags to RegexGene.
lmasroca Jul 13, 2026
0540b2d
Added MultiCharacterRange.contains(Char) method.
lmasroca Jul 13, 2026
7e45828
Added overrides for new interface RxAbsorbable methods for some regex…
lmasroca Jul 13, 2026
b0f694b
Updated RxAbsorbable, added some methods and overrides.
lmasroca Jul 15, 2026
ac93549
Added lookaheads to java regex grammar.
lmasroca Jul 15, 2026
a58d4e2
Added AssertionRepairWalk util and missing RxAbsorbable overrides.
lmasroca Jul 16, 2026
919485d
Removed unused enum.
lmasroca Jul 16, 2026
d1ed3a3
Replaced Pair usage with new data class.
lmasroca Jul 16, 2026
9d6df0d
Added check and repair logic to RegexGene for JVM regexes.
lmasroca Jul 16, 2026
ef26857
Passed original java regex string with no preprocessing to RegexGene …
lmasroca Jul 16, 2026
7a3aab6
Added a comment.
lmasroca Jul 16, 2026
564235b
Added tests for simple lookaheads.
lmasroca Jul 16, 2026
a4d4a9d
Merge remote-tracking branch 'origin/java-regex-quote-bug' into java-…
lmasroca Jul 17, 2026
fcfc949
Merge clean-up: re-added lookaheads to the java regex grammar.
lmasroca Jul 17, 2026
714fb6b
Moved java Pattern compilation to RegexGene initialization.
lmasroca Jul 18, 2026
7874c1d
Re-structured lookahead tests.
lmasroca Jul 18, 2026
f3b0fed
Made assertion repairs only run when regex contains assertions.
lmasroca Jul 20, 2026
2c22705
Added braces.
lmasroca Jul 20, 2026
736e1f1
Made canBeZeroWidth a "val" instead of "fun".
lmasroca Jul 20, 2026
7f39dd1
Enabled disabled test.
lmasroca Jul 20, 2026
5eaaf78
Cleared up some comments.
lmasroca Jul 20, 2026
b28e279
Requested changes.
lmasroca Jul 22, 2026
0986eaa
Requested changes 2: more javadocs.
lmasroca Jul 22, 2026
d7c8b87
Merge remote-tracking branch 'origin/master' into java-regex-simple-a…
lmasroca Jul 22, 2026
49ea960
Removed unnecessary code.
lmasroca Jul 23, 2026
972800b
Made repair loop check full absorbability before forcing begins, avoi…
lmasroca Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ CharacterClassEscape
: SLASH [dDsSwWvVhH]
;

EQUAL : '=';
CARET : '^';
DOLLAR : '$';
SLASH : '\\';
Expand All @@ -96,7 +97,7 @@ COLON : ':';

BaseChar
// practically all chars but the ones used for control and digits
: ~[0-9:,^$\\.*+?()[\]{}|-]
: ~[0-9:,^$\\.*+?()[\]{}|=-]
;

fragment OctalEscapeSequence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ assertion
//TODO
//// | '\\' 'b'
//// | '\\' 'B'
//// | '(' '?' '=' disjunction ')'
| PAREN_open QUESTION EQUAL disjunction PAREN_close
//// | '(' '?' '!' disjunction ')'
;

Expand Down Expand Up @@ -116,7 +116,7 @@ patternCharacter
// These are also allowed as literals when no matching pair exists
| BRACE_close
| BRACKET_close
| COLON
| COLON | EQUAL
| DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on.
;

Expand Down Expand Up @@ -168,7 +168,7 @@ classAtomNoDash
| DecimalDigit
| COMMA | CARET | DOLLAR | DOT | STAR | PLUS | QUESTION
| PAREN_open | PAREN_close | BRACKET_open | BRACE_open | BRACE_close | OR
| COLON
| COLON | EQUAL
// should be interpreted literally:
// As they are lexer tokens, these character sequences are captured as such. In particular these require some extra
// steps to interpret them correctly given the context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ private const val EOF_TOKEN = "<EOF>"
/**
* Created by arcuri82 on 11-Sep-19.
*/
class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaParserBaseVisitor<VisitResult>(){
class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaParserBaseVisitor<VisitResult>(){

private val hexEscapePrefixes = setOf('x', 'u')

Expand Down Expand Up @@ -60,6 +60,8 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
*/
private var currentFlags = externalRegexFlags

private var hasAssertions = false

/**
* Builds DisjunctionListRxGenes from a disjunction context, returns null if disjunction is unsatisfiable.
*/
Expand Down Expand Up @@ -87,6 +89,23 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
return disjList
}

/**
* Walks up [ctx]'s ancestry towards the top-level pattern, searching for one of
* the currently-unsupported ways an assertion's ancestry can appear (nested).
* Returns true if it reaches the top-level pattern without hitting either, false otherwise.
*/
private fun isAssertionNested(ctx: RegexJavaParser.AssertionContext): Boolean {
var current = ctx.parent
while (current != null && current !is RegexJavaParser.PatternContext) {
if (current is RegexJavaParser.AssertionContext
|| (current is RegexJavaParser.AtomContext && current.disjunction() != null)) {
return true
}
current = current.parent
}
return false
}

override fun visitPattern(ctx: RegexJavaParser.PatternContext): VisitResult {

val res = ctx.disjunction().accept(this)
Expand All @@ -107,8 +126,10 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
val gene = RegexGene(
"regex",
disjList,
text.substring(0, text.length - EOF_TOKEN.length),
RegexType.JVM
sourceRegex,
RegexType.JVM,
externalRegexFlags = externalRegexFlags,
hasAssertions = hasAssertions
)

return VisitResult(gene)
Expand Down Expand Up @@ -231,7 +252,18 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
val res = VisitResult()

if(ctx.assertion() != null){
res.data = ctx.assertion().text
val assertionCtx = ctx.assertion()
if (assertionCtx.CARET() != null || assertionCtx.DOLLAR() != null) {
res.data = ctx.assertion().text
} else {
require(!isAssertionNested(ctx.assertion())){
"Nested assertions are not currently supported."
}
val innerDisjList = buildDisjunctionList(assertionCtx.disjunction())
val assertionGene = AssertionRxGene(innerDisjList)
hasAssertions = true
res.genes.add(assertionGene)
}
return res
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object RegexHandler {

val pattern = parser.pattern()

val res = GeneRegexJavaVisitor(externalRegexFlags).visit(pattern)
val res = GeneRegexJavaVisitor(regex, externalRegexFlags).visit(pattern)

val gene= res.genes.first() as RegexGene
cacheJVM[key] = gene.copy() as RegexGene
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,38 @@ class AnyCharacterRxGene(
return true
}

/**
* 1 if [value]'s first character is within [validRanges] (i.e. one `.` itself could
* render, given the current flags), else 0.
* @see [RxAbsorbable.absorbableCount]
*/
override fun absorbableCount(value: String): Int {
Comment thread
lmasroca marked this conversation as resolved.
if (value.isEmpty()) {
return 0
}
return if (validRanges.contains(value[0])) {
1
} else {
0
}
}

/** Always false: `.` always renders exactly one character.
* @see [RxAbsorbable.canBeZeroWidth]
*/
override val canBeZeroWidth: Boolean = false

/**
* Forces [value]'s first character onto this gene if `.` could render it; mirrors
* [absorbableCount], so it never mutates when [absorbableCount] would return 0.
* @see [RxAbsorbable.tryForce]
*/
override fun tryForce(value: String): Int {
require(value.isNotEmpty())
val n = absorbableCount(value)
if (n == 1) {
this.value = value[0]
}
return n
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.evomaster.core.search.gene.regex

import org.evomaster.core.output.OutputFormat
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.root.SimpleGene
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.service.AdaptiveParameterControl
import org.evomaster.core.search.service.Randomness
import org.evomaster.core.search.service.mutator.MutationWeightControl
import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo
import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy

/**
* Represents a zero-width assertion in the regex gene tree.
*
* This gene is placed in the tree at the position where the assertion appeared in the
* source regex. It produces no characters ([getValueAsPrintableString] always returns
* "").
*
* Repair is triggered from [DisjunctionRxGene.attemptAssertionRepair], invoked by
* [RegexGene.randomize] after the disjunction's own sampled value is checked against
* the source pattern and found not to match.
*/
class AssertionRxGene(
/**
* The assertion's inner disjunction gene, can be null as the disjunction can be unsatisfiable,
* in that case [innerGene] is null.
*/
val innerGene: DisjunctionListRxGene?
Comment thread
lmasroca marked this conversation as resolved.
) : RxTerm, SimpleGene("assertion") {

override fun checkForLocallyValidIgnoringChildren(): Boolean = true

override fun isUnsatisfiable(): Boolean = innerGene == null

override fun isMutable(): Boolean = innerGene?.isMutable() ?: false

override fun copyContent(): Gene {
val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene)
copy.name = this.name
return copy
}

override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) {
innerGene?.randomize(randomness, tryToForceNewValue)
}

override fun setValueWithRawString(value: String) {
innerGene?.setFromStringValue(value)
}

override fun shallowMutate(
randomness: Randomness,
apc: AdaptiveParameterControl,
mwc: MutationWeightControl,
selectionStrategy: SubsetGeneMutationSelectionStrategy,
enableAdaptiveGeneMutation: Boolean,
additionalGeneMutationInfo: AdditionalGeneMutationInfo?
): Boolean {
return false
}

/**
* Zero-width: never contributes characters to the generated string.
*/
override fun getValueAsPrintableString(
previousGenes: List<Gene>,
mode: GeneUtils.EscapeMode?,
targetFormat: OutputFormat?,
extraCheck: Boolean
): String = ""

/**
* Returns the value currently sampled from [innerGene], or null if there is no
* inner gene to sample from.
*/
fun sampledInnerValue(): String? {
if (innerGene == null) {
return null
}
return innerGene.getValueAsPrintableString(targetFormat = null)
}

override fun containsSameValueAs(other: Gene): Boolean {
if (other !is AssertionRxGene) {
return false
}
return sampledInnerValue() == other.sampledInnerValue()
}

override fun unsafeCopyValueFrom(other: Gene): Boolean {
if (other !is AssertionRxGene) {
return false
}
return if (innerGene != null && other.innerGene != null) {
innerGene.unsafeCopyValueFrom(other.innerGene)
} else {
innerGene == null && other.innerGene == null
}
}

/**
* Always true: an assertion never renders characters ([getValueAsPrintableString] is
* always ""), so it can always collapse to zero width.
* @see [RxAbsorbable.canBeZeroWidth]
*/
override val canBeZeroWidth: Boolean = true

/**
* Always 0: an assertion never absorbs candidate text into itself, it only supplies a
* candidate for its siblings to absorb, via [sampledInnerValue].
* @see [RxAbsorbable.tryForce]
*/
override fun tryForce(value: String): Int {
require(value.isNotEmpty())
return 0
}

/**
* No-op: already always zero-width, so there's nothing to force.
* @see [RxAbsorbable.forceZeroWidth]
*/
override fun forceZeroWidth() {
// already always zero-width, nothing to do
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,20 @@ class BackReferenceRxGene(
// nothing to copy, as the value comes from the capture group
return containsSameValueAs(other)
}

/**
* Returns false as we do not want backreferences to mutate a previous group.
* @see [RxAbsorbable.canBeZeroWidth]
*/
override val canBeZeroWidth: Boolean = false

/**
* Always 0: a backreference's value is derived entirely from a previous [captureGroup],
* so unlike an ordinary leaf it can not be forced to absorb arbitrary candidate text.
* @see [RxAbsorbable.tryForce]
*/
override fun tryForce(value: String): Int {
require(value.isNotEmpty())
return 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ class CharacterClassEscapeRxGene(
companion object{
private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java)

private fun Char.swapCase(): Char =
if (this.isUpperCase()) {
this.lowercaseChar()
} else {
this.uppercaseChar()
}

private val digitSet = listOf(CharacterRange('0', '9'))
private val asciiLetterSet = listOf(CharacterRange(FIRST_LOWER_CASE_CHAR, LAST_LOWER_CASE_CHAR),
CharacterRange(FIRST_UPPER_CASE_CHAR, LAST_UPPER_CASE_CHAR))
Expand Down Expand Up @@ -352,4 +359,49 @@ class CharacterClassEscapeRxGene(
return false
}

/**
* 1 if [value]'s first character (or its case-swapped counterpart, when caseable) is
* within [multiCharRange], else 0.
* @see [RxAbsorbable.absorbableCount]
*/
override fun absorbableCount(value: String): Int {
if (value.isEmpty()) {
return 0
}
val c = value[0]
if (multiCharRange.contains(c)) {
return 1
}
if (flags.isCaseable(c) && multiCharRange.contains(c.swapCase())) {
return 1
}
return 0
}

/**
* Always false: a character-class escape always renders exactly one character.
* @see [RxAbsorbable.canBeZeroWidth]
*/
override val canBeZeroWidth: Boolean = false

/**
* Forces [value]'s first character onto this gene, swapping case if that's what
* matched; mirrors [absorbableCount].
* @see [RxAbsorbable.tryForce]
*/
override fun tryForce(value: String): Int {
require(value.isNotEmpty())
val n = absorbableCount(value)
require(n == 0 || n == 1)
if (n == 1) {
val c = value[0]
this.value = (if (multiCharRange.contains(c)) {
c
} else {
c.swapCase()
}).toString()
this.useUpperCase = c.isUpperCase()
}
return n
}
}
Loading
Loading