Skip to content

Commit d309908

Browse files
committed
Merge remote-tracking branch 'origin/master' into regex-java-flag-x
# Conflicts: # core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt
2 parents a9120ed + 0a1c1cd commit d309908

9 files changed

Lines changed: 270 additions & 54 deletions

File tree

core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,22 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
3636
* Capture groups in order of appearance (1-based index -> list index 0).
3737
* Populated as the tree is walked. A backreference is only valid if it
3838
* appears after the group it references, which Java regex requires anyway.
39+
* The value is nullable to represent a captured group that is unsatisfiable,
40+
* for example when the group contains an empty character class like `([a&&b])`.
41+
* In that case the map holds null instead of a DisjunctionListRxGene.
42+
* @see buildDisjunctionList
3943
*/
4044
private val captureGroups = mutableListOf<DisjunctionListRxGene?>()
4145

4246
/**
4347
* Same as [captureGroups] but for named backreferences, which can be accessed
4448
* with their name or number.
49+
* The value is nullable to represent a captured group that is unsatisfiable,
50+
* for example when the group contains an empty character class like `([a&&b])`.
51+
* In that case the map holds null instead of a DisjunctionListRxGene.
52+
* @see buildDisjunctionList
4553
*/
46-
private val namedCaptureGroups = mutableMapOf<String, DisjunctionListRxGene>()
54+
private val namedCaptureGroups = mutableMapOf<String, DisjunctionListRxGene?>()
4755

4856
/**
4957
* Tracks the flags active in the current lexical scope.
@@ -52,13 +60,48 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
5260
*/
5361
private var currentFlags = externalRegexFlags
5462

63+
/**
64+
* Builds DisjunctionListRxGenes from a disjunction context, returns null if disjunction is unsatisfiable.
65+
*/
66+
private fun buildDisjunctionList(ctx: RegexJavaParser.DisjunctionContext): DisjunctionListRxGene? {
67+
val res = ctx.accept(this)
68+
val validDisjunctions = res.genes.map { it as DisjunctionRxGene }
69+
70+
val satisfiableDisjunctions = validDisjunctions.filter{ !it.isUnsatisfiable() }
71+
72+
if(satisfiableDisjunctions.isEmpty()){
73+
// As DisjunctionListRxGene extends CompositeFixedGene, its disjunctions list cannot be empty.
74+
// In this case we return null to represent an unsatisfiable DisjunctionListRxGene.
75+
return null
76+
}
77+
78+
val disjList = DisjunctionListRxGene(satisfiableDisjunctions)
79+
80+
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
81+
for (gene in disjList.disjunctions) {
82+
gene.extraPrefix = false
83+
gene.extraPostfix = false
84+
gene.matchStart = true
85+
gene.matchEnd = true
86+
}
87+
return disjList
88+
}
89+
5590
override fun visitPattern(ctx: RegexJavaParser.PatternContext): VisitResult {
5691

5792
val res = ctx.disjunction().accept(this)
5893

5994
val text = RegexUtils.getRegexExpByParserRuleContext(ctx)
6095

61-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
96+
val satisfiableDisjunctions = res.genes
97+
.map { it as DisjunctionRxGene }
98+
.filter{ !it.isUnsatisfiable() }
99+
100+
if (satisfiableDisjunctions.isEmpty()) {
101+
throw IllegalStateException("Regex is unsatisfiable.")
102+
}
103+
104+
val disjList = DisjunctionListRxGene(satisfiableDisjunctions)
62105

63106
// we remove the <EOF> token from end of the string to store as sourceRegex
64107
val gene = RegexGene(
@@ -79,9 +122,19 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
79122
val matchStart = assertionMatches.first
80123
val matchEnd = assertionMatches.second
81124

82-
val disj = DisjunctionRxGene("disj", altRes.genes.map { it }, matchStart, matchEnd)
125+
val res = VisitResult()
126+
127+
// add disjunction if it has genes, OR if the alternative was purely assertions (^$) or flag scopes
128+
// in that case altRes.genes is empty but the alternative is valid (matches "")
129+
val hasOnlyAssertionsOrFlagScopes = ctx.alternative().term().isNotEmpty() &&
130+
ctx.alternative().term().all { it.assertion() != null || it.FLAG_SCOPE_OPEN() != null }
83131

84-
val res = VisitResult(disj)
132+
if (altRes.genes.isNotEmpty() || hasOnlyAssertionsOrFlagScopes || ctx.alternative().term().isEmpty()) {
133+
val disj = DisjunctionRxGene("disj", altRes.genes.map { it }, matchStart, matchEnd)
134+
135+
res.genes.add(disj)
136+
}
137+
// else: had non-assertion terms but all produced nothing (empty char class etc.), skip
85138

86139
if(ctx.disjunction() != null){
87140
val disjRes = ctx.disjunction().accept(this)
@@ -147,7 +200,7 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
147200
// term is not a back ref: we use the default behavior, term results may only have 0-1 genes
148201
// if there is a gene, we add it to result
149202
res.genes.add(gene)
150-
} else {
203+
} else if (resTerm.data is String) {
151204

152205
val assertion = resTerm.data as String
153206
if(i==0 && assertion == "^"){
@@ -162,6 +215,9 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
162215
*/
163216
throw IllegalStateException("Cannot support $assertion at position $i")
164217
}
218+
} else {
219+
// unsatisfiable term, return with no genes
220+
return VisitResult(data=Pair(false, false))
165221
}
166222
}
167223

@@ -181,12 +237,23 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
181237

182238
val resAtom = ctx.atom().accept(this)
183239
val atom = resAtom.genes.firstOrNull()
184-
?: return res
185240

186241
if(ctx.quantifier() != null){
187242

188243
val limits = ctx.quantifier().accept(this).data as Pair<Int,Int>
189244

245+
// if quantified atom is unsatisfiable we must then check the limits
246+
if (atom == null ||
247+
((atom as? RxTerm)?.isUnsatisfiable() == true) && resAtom.genes.size == 1) {
248+
return if (limits.first == 0) {
249+
// if 0 appearances is allowed then the regex is satisfiable only with empty string
250+
VisitResult(PatternCharacterBlockGene("0_QuantifierOnEmptyRegex", ""))
251+
} else {
252+
// if not then unsatisfiable, return with no genes
253+
res
254+
}
255+
}
256+
190257
// if atom is not a back ref then we use the default behavior, results may only have one gene
191258
var template: Gene = atom
192259

@@ -211,10 +278,11 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
211278
if (ctx.atom()?.atomEscape()?.BackReference() != null){
212279
// if atom is a BackReference we addAll genes from result as there may be more than one if digits are dropped
213280
res.genes.addAll(resAtom.genes)
214-
} else {
281+
} else if (atom != null) {
215282
// if atom is not a back ref we fall back to the default behavior, results only have one gene
216283
res.genes.add(atom)
217284
}
285+
// else atom is unsatisfiable, return no genes
218286
}
219287

220288
return res
@@ -284,21 +352,16 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
284352

285353
currentFlags = merged
286354

287-
val res = ctx.disjunction().accept(this)
355+
val disjList = buildDisjunctionList(ctx.disjunction())
288356

289357
currentFlags = previous
290358

291-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
292-
293-
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
294-
for (gene in disjList.disjunctions) {
295-
gene.extraPrefix = false
296-
gene.extraPostfix = false
297-
gene.matchStart = true
298-
gene.matchEnd = true
359+
return if (disjList != null) {
360+
VisitResult(disjList)
361+
} else {
362+
// unsatisfiable, return with no genes.
363+
VisitResult()
299364
}
300-
301-
return VisitResult(disjList)
302365
}
303366

304367
if(ctx.quote() != null){
@@ -332,17 +395,7 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
332395
val groupIndex = captureGroups.size
333396
captureGroups.add(null) // add placeholder for the gene
334397

335-
val res = ctx.disjunction().accept(this)
336-
337-
val disjList = DisjunctionListRxGene(res.genes.map { it as DisjunctionRxGene })
338-
339-
//TODO tmp hack until full handling of ^$. Assume full match when nested disjunctions
340-
for(gene in disjList.disjunctions){
341-
gene.extraPrefix = false
342-
gene.extraPostfix = false
343-
gene.matchStart = true
344-
gene.matchEnd = true
345-
}
398+
val disjList = buildDisjunctionList(ctx.disjunction())
346399

347400
val isCapturingGroup = !ctx.text.startsWith("(?:")
348401
val isNamedCaptureGroup = ctx.NAMED_CAPTURE_GROUP_OPEN() != null
@@ -358,7 +411,12 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
358411
namedCaptureGroups[name] = disjList
359412
}
360413

361-
return VisitResult(disjList)
414+
return if (disjList != null) {
415+
VisitResult(disjList)
416+
} else {
417+
// unsatisfiable, return with no genes.
418+
VisitResult()
419+
}
362420
}
363421

364422
if(ctx.DOT() != null){
@@ -577,15 +635,12 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
577635
maxDigits > allDigits.length -> allDigits.length
578636
allDigits.take(maxDigits).toInt() <= captureGroups.size -> maxDigits
579637
maxDigits > 1 -> maxDigits - 1
580-
else -> throw IllegalStateException(
581-
"Backreference ${txt.take(2)} refers to group ${allDigits[0]} but only ${captureGroups.size} " +
582-
"capture group(s) have been defined so far"
583-
)
638+
else -> 1
584639
}
585640

586641
val n = allDigits.take(backRefDigitCount).toInt()
587642

588-
val result = VisitResult(BackReferenceRxGene(n, captureGroups[n - 1]!!))
643+
val result = VisitResult(BackReferenceRxGene(n, captureGroups.getOrNull(n - 1)))
589644

590645
val remainingChars = allDigits.drop(backRefDigitCount)
591646

@@ -601,8 +656,10 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
601656
if (ctx.NamedBackReference() != null) {
602657
// strip "\k<" and ">"
603658
val name = txt.drop(3).dropLast(1)
659+
if(name !in namedCaptureGroups){
660+
throw IllegalStateException("Named backreference \\k<$name> refers to unknown group '$name'")
661+
}
604662
val group = namedCaptureGroups[name]
605-
?: throw IllegalStateException("Named backreference \\k<$name> refers to unknown group '$name'")
606663
val groupIndex = captureGroups.indexOf(group) + 1 // 1-based, for the gene name
607664
return VisitResult(BackReferenceRxGene(groupIndex, group))
608665
}

core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation
1414
* Represents a backreference \N in a regex (N being a number).
1515
* Its value is always identical to the current value of its [captureGroup].
1616
* It has no independent state and is therefore immutable.
17+
* If capture group is null then the referenced group was unsatisfiable,
18+
* in which case the same is true for the backreference to it.
1719
*/
1820
class BackReferenceRxGene(
1921
val groupIndex: Int,
20-
val captureGroup: DisjunctionListRxGene
22+
val captureGroup: DisjunctionListRxGene?
2123
) : RxAtom, SimpleGene("\\$groupIndex") {
2224

25+
override fun isUnsatisfiable(): Boolean {
26+
return captureGroup == null || captureGroup.isUnsatisfiable()
27+
}
28+
2329
override fun checkForLocallyValidIgnoringChildren(): Boolean = true
2430

2531
/**
@@ -59,7 +65,12 @@ class BackReferenceRxGene(
5965
mode: GeneUtils.EscapeMode?,
6066
targetFormat: OutputFormat?,
6167
extraCheck: Boolean
62-
): String = captureGroup.getValueAsPrintableString(targetFormat = null)
68+
): String {
69+
if (captureGroup == null) {
70+
throw IllegalStateException("Cannot get value from invalid backreference \\$groupIndex")
71+
}
72+
return captureGroup.getValueAsPrintableString(previousGenes, mode, targetFormat)
73+
}
6374

6475
override fun containsSameValueAs(other: Gene): Boolean {
6576
if (other !is BackReferenceRxGene) return false

core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class CharacterClassEscapeRxGene(
8282
// create both normal and negated version for all
8383
.flatMap { (key, value) ->
8484
listOf(
85-
key to MultiCharacterRange(value),
85+
key to MultiCharacterRange(false, value),
8686
"^$key" to MultiCharacterRange(true, value)
8787
)
8888
}.toMap()
@@ -132,6 +132,12 @@ class CharacterClassEscapeRxGene(
132132
}
133133
}
134134

135+
override fun isUnsatisfiable(): Boolean = multiCharRange.isEmpty
136+
137+
override fun isMutable(): Boolean {
138+
return !isUnsatisfiable()
139+
}
140+
135141
override fun checkForLocallyValidIgnoringChildren() : Boolean{
136142
// we pass the same embedded flags to the regex to accurately match the expected behavior
137143
return value.matches(Regex("${flags.getScopeString()}\\$type"))
@@ -193,6 +199,9 @@ class CharacterClassEscapeRxGene(
193199
}
194200

195201
override fun getValueAsPrintableString(previousGenes: List<Gene>, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String {
202+
if (isUnsatisfiable()) {
203+
throw IllegalStateException("Cannot get value from empty CharacterClassEscape")
204+
}
196205
return if (!flags.isCaseable(value[0])) {
197206
value[0].toString()
198207
}

core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,19 @@ class CharacterRangeRxGene(
3030
private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java)
3131
}
3232

33-
var value : Char = validRanges[0].start
33+
// '\u0000' is a placeholder for the unsatisfiable case (empty MCR with no valid ranges).
34+
// This gene will never be randomized or mutated when isUnsatisfiable() is true, as it would be immutable.
35+
// getValueAsPrintableString throws when isUnsatisfiable, so that value should be unreachable.
36+
var value : Char = if (isUnsatisfiable()) '\u0000' else validRanges[0].start
3437

3538
/**
3639
* Whether to output the character in uppercase.
3740
* Only meaningful when flags.caseInsensitive is true.
3841
*/
3942
var useUpperCase: Boolean = false
4043

44+
override fun isUnsatisfiable(): Boolean = validRanges.isEmpty
45+
4146
override fun checkForLocallyValidIgnoringChildren() : Boolean{
4247
return validRanges.any {
4348
value in it ||
@@ -49,6 +54,9 @@ class CharacterRangeRxGene(
4954
}
5055

5156
override fun isMutable(): Boolean {
57+
if (isUnsatisfiable()) {
58+
return false
59+
}
5260
// check if there is more than one character or if the character is caseable
5361
return validRanges.charCount > 1 || flags.isCaseable(value)
5462
}
@@ -134,6 +142,9 @@ class CharacterRangeRxGene(
134142
TODO should \ be handled specially?
135143
In any case, would have same handling as AnyCharacterRxGene
136144
*/
145+
if (isUnsatisfiable()) {
146+
throw IllegalStateException("Cannot get value from empty CharacterRange")
147+
}
137148
return if (!flags.isCaseable(value)) {
138149
value.toString()
139150
}

core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class DisjunctionRxGene(
4747
private val log : Logger = LoggerFactory.getLogger(DisjunctionRxGene::class.java)
4848
}
4949

50+
override fun isUnsatisfiable(): Boolean =
51+
terms.isNotEmpty() && terms.any { (it as? RxTerm)?.isUnsatisfiable() == true }
5052

5153
override fun checkForLocallyValidIgnoringChildren() : Boolean{
5254
return true

core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class QuantifierRxGene(
4949
if (min < 0) {
5050
throw IllegalArgumentException("Invalid min value '$min': should be positive")
5151
}
52-
if (max < 1) {
53-
throw IllegalArgumentException("Invalid max value '$max': should be at least 1")
52+
if (max < 0) {
53+
throw IllegalArgumentException("Invalid max value '$max': should be positive")
5454
}
5555
if (min > max) {
5656
throw IllegalArgumentException("Invalid min-max values '$min-$max': min is greater than max")

core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxTerm.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ import org.evomaster.core.search.StructuralElement
44
import org.evomaster.core.search.gene.Gene
55

66

7-
interface RxTerm
7+
interface RxTerm {
8+
/**
9+
* Returns true if this gene can never produce a valid value,
10+
* for example an empty character class intersection like [a&&b].
11+
* Used at construction time to filter unsatisfiable branches from disjunctions.
12+
*/
13+
fun isUnsatisfiable(): Boolean = false
14+
}

0 commit comments

Comments
 (0)