Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better mutant descriptions #268

Merged
merged 11 commits into from
Dec 12, 2023
Merged
8 changes: 5 additions & 3 deletions core/src/main/scala/weaponregex/model/Location.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import scala.scalajs.js.annotation.*
/** A location in the source code which can span multiple lines and/or columns.
*
* @param start
* start [[weaponregex.model.Position]]
* start [[weaponregex.model.Position]] (inclusive)
* @param end
* end [[weaponregex.model.Position]]
* end [[weaponregex.model.Position]] (exclusive)
*/
@JSExportAll
case class Location(start: Position, end: Position)
case class Location(start: Position, end: Position) {
val pretty: String = s"[${start.pretty}, ${end.pretty})"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than pretty, how about show? This is also in line with the cats typeclass

Suggested change
val pretty: String = s"[${start.pretty}, ${end.pretty})"
def show: String = s"[${start.show}, ${end.show})"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 🧐 Good to know.

}

/** Companion object for [[weaponregex.model.Location]]
*/
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/scala/weaponregex/model/Position.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ import scala.scalajs.js.annotation.*
* column number
*/
@JSExportAll
case class Position(line: Int, column: Int)
case class Position(line: Int, column: Int) {
def pretty: String = s"$line:$column"
}
62 changes: 41 additions & 21 deletions core/src/main/scala/weaponregex/model/mutation/TokenMutator.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
package weaponregex.model.mutation

import weaponregex.extension.RegexTreeExtension.RegexTreeStringBuilder
import weaponregex.model.Location
import weaponregex.model.regextree.RegexTree
import weaponregex.model.regextree.Node
import weaponregex.model.regextree.{Node, RegexTree}

trait TokenMutator {

/** The name of the mutator
*/
def name: String
val name: String

/** The mutation levels that the token mutator falls under
*/
def levels: Seq[Int]
val levels: Seq[Int]

@deprecated(
"This member is deprecated and will be removed in a later version. " +
"For now, this will return the same value as `name`." +
"Description is now generated for each mutant instead of the mutator.",
"v1.2.0"
)
/** A short description of the mutator
*/
def description: String
def description: String = name
nhaajt marked this conversation as resolved.
Show resolved Hide resolved

/** Generate the default description for the mutants of this mutator
* @param original
* The original token string being mutated
* @param mutated
* The mutated string
* @param location
* The [[weaponregex.model.Location]] where the mutation occurred
*/
def description(original: String, mutated: String, location: Location): String =
s"${location.pretty} Mutate $original to $mutated"

/** Apply mutation to the given token
* @param token
Expand All @@ -41,24 +58,24 @@ trait TokenMutator {
*/
implicit protected class MutatedPatternExtension(pattern: String) {

/** Convert a mutated pattern string into a [[weaponregex.model.mutation.Mutant]] at the provided
* [[weaponregex.model.Location]]
* @param location
* [[weaponregex.model.Location]] of the mutation
* @return
* A [[weaponregex.model.mutation.Mutant]]
*/
def toMutantAt(location: Location): Mutant =
Mutant(pattern, name, location, levels, description)

/** Convert a mutated pattern string into a [[weaponregex.model.mutation.Mutant]] with the
* [[weaponregex.model.Location]] taken from the provided token
* @param token
* The token for reference
* @param location
* The [[weaponregex.model.Location]] where the mutation occurred. If not provided, this will be taken from the
* provided token.
* @param description
* The description of the mutant. If not provided, the default description of the mutator is used instead.
* @return
* A [[weaponregex.model.mutation.Mutant]]
*/
def toMutantOf(token: RegexTree): Mutant = toMutantAt(token.location)
def toMutantOf(token: RegexTree, location: Location = null, description: String = null): Mutant = {
val loc: Location = if (location == null) token.location else location
val desc: String =
if (description == null) TokenMutator.this.description(token.build, pattern, loc) else description
hugo-vrijswijk marked this conversation as resolved.
Show resolved Hide resolved
Mutant(pattern, name, loc, levels, desc)
}

/** Convert a mutated pattern string into a [[weaponregex.model.mutation.Mutant]] with the
* [[weaponregex.model.Location]] starts from the start of the provided token and ends at the start of the token's
Expand All @@ -67,15 +84,17 @@ trait TokenMutator {
* If the given token has no child, the location of the given token is considered to be the location of the mutant
* @param token
* The token for reference
* @param description
* The description of the mutant. If not provided, the default description of the mutator is used instead.
* @return
* A [[weaponregex.model.mutation.Mutant]]
*/
def toMutantBeforeChildrenOf(token: RegexTree): Mutant = {
def toMutantBeforeChildrenOf(token: RegexTree, description: String = null): Mutant = {
val loc: Location = token match {
case node: Node if node.children.nonEmpty => Location(token.location.start, node.children.head.location.start)
case _ => token.location
}
toMutantAt(loc)
toMutantOf(token, loc, description)
}

/** Convert a mutated pattern string into a [[weaponregex.model.mutation.Mutant]] with the
Expand All @@ -85,16 +104,17 @@ trait TokenMutator {
* If the given token has no child, the location of the given token is considered to be the location of the mutant
* @param token
* The token for reference
* @param description
* The description of the mutant. If not provided, the default description of the mutator is used instead.
* @return
* A [[weaponregex.model.mutation.Mutant]]
*/
def toMutantAfterChildrenOf(token: RegexTree): Mutant = {
def toMutantAfterChildrenOf(token: RegexTree, description: String = null): Mutant = {
val loc: Location = token match {
case node: Node if node.children.nonEmpty => Location(node.children.last.location.end, token.location.end)
case _ => token.location
}

toMutantAt(loc)
toMutantOf(token, loc, description)
}
}
}
25 changes: 15 additions & 10 deletions core/src/main/scala/weaponregex/mutator/boundaryMutator.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package weaponregex.mutator

import weaponregex.extension.RegexTreeExtension.RegexTreeStringBuilder
import weaponregex.model.Location
import weaponregex.model.mutation.{Mutant, TokenMutator}
import weaponregex.model.regextree.*

/** Remove beginning of line character `^`
/** Mutator for beginning of line character `^` removal
*
* ''Mutation level(s):'' 1, 2, 3
* @example
Expand All @@ -13,7 +14,8 @@ import weaponregex.model.regextree.*
object BOLRemoval extends TokenMutator {
override val name: String = "Beginning of line character `^` removal"
override val levels: Seq[Int] = Seq(1, 2, 3)
override val description: String = "Remove beginning of line character `^`"
override def description(original: String, mutated: String, location: Location): String =
location.pretty + " Remove the beginning of line character `^`"

override def mutate(token: RegexTree): Seq[Mutant] = token match {
case node: Node =>
Expand All @@ -25,7 +27,7 @@ object BOLRemoval extends TokenMutator {
}
}

/** Remove end of line character `$`
/** Mutator for end of line character `$` removal
*
* ''Mutation level(s):'' 1, 2, 3
* @example
Expand All @@ -34,7 +36,8 @@ object BOLRemoval extends TokenMutator {
object EOLRemoval extends TokenMutator {
override val name: String = "End of line character `$` removal"
override val levels: Seq[Int] = Seq(1, 2, 3)
override val description: String = "Remove end of line character `$`"
override def description(original: String, mutated: String, location: Location): String =
location.pretty + " Remove the end of line character `$`"

override def mutate(token: RegexTree): Seq[Mutant] = token match {
case node: Node =>
Expand All @@ -46,33 +49,35 @@ object EOLRemoval extends TokenMutator {
}
}

/** Change beginning of line `^` to beginning of input `\A`
/** Mutator for beginning of line `^` to beginning of input `\A` change
*
* ''Mutation level(s):'' 2, 3
* @example
* `^a` ⟶ `\Aa`
*/
object BOL2BOI extends TokenMutator {
override val name: String = """Beginning of line `^` to beginning of input `\A`"""
override val name: String = """Beginning of line `^` to beginning of input `\A` change"""
override val levels: Seq[Int] = Seq(2, 3)
override val description: String = """Change beginning of line `^` to beginning of input `\A`"""
override def description(original: String, mutated: String, location: Location): String =
location.pretty + """ Change the beginning of line `^` to beginning of input `\A`"""

override def mutate(token: RegexTree): Seq[Mutant] = (token match {
case _: BOL => Seq(Boundary("A", token.location))
case _ => Nil
}) map (_.build.toMutantOf(token))
}

/** Change end of line `$` to end pf input `\z`
/** Mutator for end of line `$` to end of input `\z` change
*
* ''Mutation level(s):'' 2, 3
* @example
* `a$` ⟶ `a\z`
*/
object EOL2EOI extends TokenMutator {
override val name: String = """End of line `$` to end of input `\z`"""
override val name: String = """End of line `$` to end of input `\z` change"""
override val levels: Seq[Int] = Seq(2, 3)
override val description: String = """Change end of line `$` to end of input `\z`"""
override def description(original: String, mutated: String, location: Location): String =
location.pretty + """ Change the end of line `$` to end of input `\z`"""

override def mutate(token: RegexTree): Seq[Mutant] = (token match {
case _: EOL => Seq(Boundary("z", token.location))
Expand Down
15 changes: 9 additions & 6 deletions core/src/main/scala/weaponregex/mutator/capturingMutator.scala
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
package weaponregex.mutator

import weaponregex.extension.RegexTreeExtension.RegexTreeStringBuilder
import weaponregex.model.Location
import weaponregex.model.mutation.{Mutant, TokenMutator}
import weaponregex.model.regextree.*

/** Modify capturing group to non-capturing group
/** Mutator for capturing group to non-capturing group modification
*
* ''Mutation level(s):'' 2, 3
* @example
* `(abc)` ⟶ `(?:abc)`
*/
object GroupToNCGroup extends TokenMutator {
override val name: String = "Capturing group to non-capturing group"
override val name: String = "Capturing group to non-capturing group modification"
override val levels: Seq[Int] = Seq(2, 3)
override val description: String = "Modify capturing group to non-capturing group"
override def description(original: String, mutated: String, location: Location): String =
s"${location.pretty} Modify the capturing group `$original` to non-capturing group `$mutated`"

override def mutate(token: RegexTree): Seq[Mutant] = (token match {
case group @ Group(_, true, _) => Seq(group.copy(isCapturing = false))
case _ => Nil
}) map (_.build.toMutantBeforeChildrenOf(token))
}

/** Negate lookaround (lookahead, lookbehind) constructs
/** Mutator for lookaround constructs (lookahead, lookbehind) negation
*
* ''Mutation level(s):'' 1, 2, 3
* @example
* `(?=abc)` ⟶ `(?!abc)`
*/
object LookaroundNegation extends TokenMutator {
override val name: String = "Lookaround constructs negation"
override val name: String = "Lookaround constructs (lookahead, lookbehind) negation"
override val levels: Seq[Int] = Seq(1, 2, 3)
override val description: String = "Negate lookaround constructs (lookahead, lookbehind)"
override def description(original: String, mutated: String, location: Location): String =
s"${location.pretty} Negate the lookaround construct `$original` to `$mutated`"

override def mutate(token: RegexTree): Seq[Mutant] = (token match {
case la: Lookaround => Seq(la.copy(isPositive = !la.isPositive))
Expand Down
Loading