d.core.2024.9.2.source-code.Mutation.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Sandboxing and code analysis toolkit for CS 124.
The newest version!
@file:Suppress("MatchingDeclarationName", "TooManyFunctions")
package edu.illinois.cs.cs125.jeed.core
import com.squareup.moshi.JsonClass
import org.apache.commons.text.StringEscapeUtils
import java.util.Objects
import kotlin.math.abs
import kotlin.math.pow
import kotlin.random.Random
sealed class Mutation(
val mutationType: Type,
var location: Location,
val original: String,
val fileType: Source.FileType,
) {
@JsonClass(generateAdapter = true)
data class Location(
val start: Int,
val end: Int,
val line: String,
val startLine: Int,
val endLine: Int,
) {
init {
check(end >= start) { "Invalid location: $end $start" }
}
data class SourcePath(val type: Type, val name: String) {
enum class Type { CLASS, METHOD }
}
fun shift(amount: Int) = copy(start = start + amount, end = end + amount)
override fun equals(other: Any?) = when {
this === other -> true
javaClass != other?.javaClass -> false
else -> {
other as Location
start == other.start && end == other.end && line == other.line
}
}
override fun hashCode() = Objects.hash(start, end, line)
}
fun overlaps(other: Mutation) =
(other.location.start in location.start..location.end) ||
(other.location.end in location.start..location.end) ||
(other.location.start < location.start && location.end < other.location.end) ||
(location.start < other.location.start && other.location.end < location.end)
fun after(other: Mutation) = location.start > other.location.end
fun shift(amount: Int) {
location = location.shift(amount)
}
enum class Type {
BOOLEAN_LITERAL,
CHAR_LITERAL,
STRING_LITERAL,
STRING_LITERAL_LOOKALIKE,
STRING_LITERAL_CASE,
NUMBER_LITERAL,
CONDITIONAL_BOUNDARY,
NEGATE_CONDITIONAL,
SWAP_AND_OR,
INCREMENT_DECREMENT,
INVERT_NEGATION,
MATH,
PRIMITIVE_RETURN,
TRUE_RETURN,
FALSE_RETURN,
NULL_RETURN,
PLUS_TO_MINUS,
REMOVE_RUNTIME_CHECK,
REMOVE_METHOD,
NEGATE_IF,
NEGATE_WHILE,
REMOVE_IF,
REMOVE_LOOP,
REMOVE_AND_OR,
REMOVE_TRY,
REMOVE_STATEMENT,
REMOVE_PLUS,
REMOVE_BINARY,
CHANGE_EQUALS,
SWAP_BREAK_CONTINUE,
PLUS_OR_MINUS_ONE_TO_ZERO,
ADD_BREAK,
STRING_LITERAL_TRIM,
NUMBER_LITERAL_TRIM,
ADD_CONTINUE,
// TODO: Finish
MODIFY_ARRAY_LITERAL,
MODIFY_LENGTH_AND_SIZE,
}
var modified: String? = null
val applied: Boolean
get() = modified != null
fun reset() {
modified = null
}
var linesChanged: Int? = null
fun apply(contents: String, random: Random = Random): String {
val wasBlank = contents.lines()[location.startLine - 1].isBlank()
val prefix = contents.substring(0 until location.start)
val target = contents.substring(location.start..location.end)
val postfix = contents.substring((location.end + 1) until contents.length)
check(prefix + target + postfix == contents) { "Didn't split string properly" }
check(target == original) { "Didn't find expected contents before mutation: $target != $original" }
check(modified == null) { "Mutation already applied" }
modified = applyMutation(random)
check(modified != original) { "Mutation did not change the input: $mutationType" }
val originalLines = original.lines()
val modifiedLines = modified!!.lines()
linesChanged = when {
modified!!.isBlank() -> original.lines().size
originalLines.size == modifiedLines.size ->
originalLines.zip(modifiedLines).filter { (m, o) -> m != o }.size
else -> abs(originalLines.size - modifiedLines.size)
}
require(linesChanged!! > 0) { "Line change count failed" }
return (prefix + modified + postfix).lines().filterIndexed { index, s ->
if (index + 1 != location.startLine) {
true
} else {
wasBlank || s.isNotBlank()
}
}.joinToString("\n")
}
abstract fun applyMutation(random: Random = Random): String
abstract val preservesLength: Boolean
abstract val estimatedCount: Int
abstract val mightNotCompile: Boolean
abstract val fixedCount: Boolean
override fun toString(): String = "$mutationType: $location ($original)"
override fun equals(other: Any?) = when {
this === other -> true
javaClass != other?.javaClass -> false
else -> {
other as Mutation
mutationType == other.mutationType && location == other.location && original == other.original
}
}
override fun hashCode() = Objects.hash(mutationType, location, original)
companion object {
inline fun find(parsedSource: Source.ParsedSource, fileType: Source.FileType): List {
@Suppress("CascadeIf")
return if (fileType == Source.FileType.JAVA) {
JavaMutationListener(parsedSource).mutations.filterIsInstance()
} else if (fileType == Source.FileType.KOTLIN) {
KotlinMutationListener(parsedSource).mutations.filterIsInstance()
} else {
error("Invalid fileType $fileType")
}
}
}
}
@JsonClass(generateAdapter = true)
data class AppliedMutation(
val mutationType: Mutation.Type,
var location: Mutation.Location,
val original: String,
val mutated: String,
val linesChanged: Int,
val mightNotCompile: Boolean,
) {
constructor(mutation: Mutation) : this(
mutation.mutationType,
mutation.location,
mutation.original,
mutation.modified!!,
mutation.linesChanged!!,
mutation.mightNotCompile,
) {
require(mutation.applied) { "Must be created from an applied mutation" }
}
}
val PITEST = setOf(
Mutation.Type.BOOLEAN_LITERAL,
Mutation.Type.CHAR_LITERAL,
Mutation.Type.STRING_LITERAL,
Mutation.Type.STRING_LITERAL_LOOKALIKE,
Mutation.Type.STRING_LITERAL_CASE,
Mutation.Type.NUMBER_LITERAL,
Mutation.Type.CONDITIONAL_BOUNDARY,
Mutation.Type.NEGATE_CONDITIONAL,
Mutation.Type.SWAP_AND_OR,
Mutation.Type.INCREMENT_DECREMENT,
Mutation.Type.INVERT_NEGATION,
Mutation.Type.MATH,
Mutation.Type.PRIMITIVE_RETURN,
Mutation.Type.TRUE_RETURN,
Mutation.Type.FALSE_RETURN,
Mutation.Type.NULL_RETURN,
Mutation.Type.PLUS_TO_MINUS,
)
val OTHER = setOf(
Mutation.Type.REMOVE_RUNTIME_CHECK,
Mutation.Type.REMOVE_METHOD,
Mutation.Type.NEGATE_IF,
Mutation.Type.REMOVE_IF,
Mutation.Type.REMOVE_LOOP,
Mutation.Type.REMOVE_AND_OR,
Mutation.Type.REMOVE_TRY,
Mutation.Type.REMOVE_STATEMENT,
Mutation.Type.REMOVE_PLUS,
Mutation.Type.REMOVE_BINARY,
Mutation.Type.CHANGE_EQUALS,
Mutation.Type.SWAP_BREAK_CONTINUE,
Mutation.Type.PLUS_OR_MINUS_ONE_TO_ZERO,
Mutation.Type.ADD_BREAK,
Mutation.Type.ADD_CONTINUE,
Mutation.Type.STRING_LITERAL_TRIM,
Mutation.Type.NUMBER_LITERAL_TRIM,
Mutation.Type.MODIFY_LENGTH_AND_SIZE,
Mutation.Type.MODIFY_ARRAY_LITERAL,
)
val ALL = PITEST + OTHER
fun Mutation.Type.suppressionComment() = "mutate-disable-" + mutationName()
fun Mutation.Type.mutationName() = name.lowercase().replace("_", "-")
class BooleanLiteral(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.BOOLEAN_LITERAL, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
"true" -> "false"
"false" -> "true"
else -> error("${this.javaClass.name} didn't find expected text")
}
}
val ALPHANUMERIC_CHARS = (('a'..'z') + ('A'..'Z') + ('0'..'9')).toSet()
class CharLiteral(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.CHAR_LITERAL, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = ALPHANUMERIC_CHARS.size - 1
override val mightNotCompile = false
override val fixedCount = false
private val character = original.removeSurrounding("'").also {
check(it.length == 1 || it.startsWith("\\")) { "Character didn't have the correct length: $original" }
}.first()
override fun applyMutation(random: Random): String =
ALPHANUMERIC_CHARS.filter { it != character }.shuffled(random).first().let { "'$it'" }
}
private val ALPHANUMERIC_CHARS_AND_SPACE = (('a'..'z') + ('A'..'Z') + ('0'..'9') + (' ')).toSet()
// private val PUNCTUATION = setOf('.', ',', '!', '?', ';', ':', '[', ']', '(', ')', '<', '>')
private val LOOKALIKES =
mapOf('0' to 'O', '0' to 'o', '1' to 'l', '.' to ',', '!' to '?', ':' to ';', '[' to '(', ']' to ')').toMutableMap()
.apply {
val keysCopy = keys.toList()
for (key in keysCopy) {
this[this[key]!!] = key
}
}.toMap().toSortedMap()
class StringLiteral(
location: Location,
original: String,
fileType: Source.FileType,
private val withQuotes: Boolean = true,
) : Mutation(Type.STRING_LITERAL, location, original, fileType) {
private val string = if (withQuotes) {
original.removeSurrounding("\"")
} else {
original
}.let {
StringEscapeUtils.unescapeJava(it)
}
override val preservesLength = string.isNotEmpty()
override val estimatedCount = ALPHANUMERIC_CHARS_AND_SPACE.size.toDouble().pow(string.length).toInt() - 1
override val mightNotCompile = false
override val fixedCount = false
@Suppress("NestedBlockDepth")
override fun applyMutation(random: Random): String = if (string.isEmpty()) {
" "
} else {
string.toCharArray().let { characters ->
val position = characters.indices.random(random)
characters[position] =
ALPHANUMERIC_CHARS_AND_SPACE.filter { it != characters[position] }.shuffled(random).first()
characters.joinToString("")
}.let {
StringEscapeUtils.escapeJava(it)
}
}.let {
if (withQuotes) {
"\"$it\""
} else {
it
}
}
}
class StringLiteralLookalike(
location: Location,
original: String,
fileType: Source.FileType,
private val withQuotes: Boolean = true,
) : Mutation(Type.STRING_LITERAL_LOOKALIKE, location, original, fileType) {
override val preservesLength = true
private val string = if (withQuotes) {
original.removeSurrounding("\"")
} else {
original
}.let {
StringEscapeUtils.unescapeJava(it)
}
override val estimatedCount =
2.0.pow(string.filter { LOOKALIKES.containsKey(it) }.length).toInt() - 1
override val mightNotCompile = false
override val fixedCount = false
@Suppress("NestedBlockDepth")
override fun applyMutation(random: Random): String = string.toCharArray().let { characters ->
val position = characters.indices.filter { LOOKALIKES.containsKey(characters[it]) }.random(random)
characters[position] = LOOKALIKES[characters[position]]!!
characters.joinToString("")
}.let {
StringEscapeUtils.escapeJava(it)
}.let {
if (withQuotes) {
"\"$it\""
} else {
it
}
}
companion object {
fun matches(contents: String, withQuotes: Boolean = true) = contents.let {
if (withQuotes) {
contents.removeSurrounding("\"")
} else {
contents
}
}.let { string ->
StringEscapeUtils.unescapeJava(string).any { LOOKALIKES.containsKey(it) }
}
}
}
class StringLiteralCase(
location: Location,
original: String,
fileType: Source.FileType,
private val withQuotes: Boolean = true,
) : Mutation(Type.STRING_LITERAL_CASE, location, original, fileType) {
override val preservesLength = true
private val string = if (withQuotes) {
original.removeSurrounding("\"")
} else {
original
}.let {
StringEscapeUtils.unescapeJava(it)
}
override val estimatedCount =
2.0.pow(
string
.split(" ")
.filter { it.isNotEmpty() && (it.first().isUpperCase() || it.first().isLowerCase()) }
.size,
).toInt() - 1
override val mightNotCompile = false
override val fixedCount = false
@Suppress("NestedBlockDepth")
override fun applyMutation(random: Random): String = string.toCharArray().let { characters ->
val position = characters.indices
.filter {
(it == 0 || characters[it - 1] == ' ') &&
(characters[it].isLowerCase() || characters[it].isUpperCase())
}
.random(random)
characters[position] = if (characters[position].isUpperCase()) {
characters[position].lowercaseChar()
} else if (characters[position].isLowerCase()) {
characters[position].uppercaseChar()
} else {
error("Bad position")
}
characters.joinToString("")
}.let {
StringEscapeUtils.escapeJava(it)
}.let {
if (withQuotes) {
"\"$it\""
} else {
it
}
}
companion object {
fun matches(contents: String, withQuotes: Boolean = true) = contents.let {
if (withQuotes) {
contents.removeSurrounding("\"")
} else {
contents
}
}.let { string ->
StringEscapeUtils.unescapeJava(string).split(" ").any {
it.isNotEmpty() && (it.first().isUpperCase() || it.first().isLowerCase())
}
}
}
}
class StringLiteralTrim(
location: Location,
original: String,
fileType: Source.FileType,
private val withQuotes: Boolean = true,
) : Mutation(Type.STRING_LITERAL_TRIM, location, original, fileType) {
override val preservesLength = false
private val string = if (withQuotes) {
original.removeSurrounding("\"")
} else {
original
}.let {
StringEscapeUtils.unescapeJava(it)
}
override val estimatedCount = 2
override val mightNotCompile = false
override val fixedCount = true
@Suppress("NestedBlockDepth")
override fun applyMutation(random: Random): String = if (random.nextBoolean()) {
string.substring(1)
} else {
string.substring(0, string.length - 1)
}.let {
StringEscapeUtils.escapeJava(it)
}.let {
if (withQuotes) {
"\"$it\""
} else {
it
}
}
companion object {
fun matches(contents: String, withQuotes: Boolean = true) = contents.let {
if (withQuotes) {
contents.removeSurrounding("\"")
} else {
contents
}
}.let {
StringEscapeUtils.unescapeJava(it).length >= 2
}
}
}
internal fun MutableList.addStringMutations(
location: Mutation.Location,
contents: String,
fileType: Source.FileType,
withQuotes: Boolean = true,
) {
add(StringLiteral(location, contents, fileType, withQuotes))
if (StringLiteralLookalike.matches(contents, withQuotes)) {
add(StringLiteralLookalike(location, contents, fileType, withQuotes))
}
if (StringLiteralCase.matches(contents, withQuotes)) {
add(StringLiteralCase(location, contents, fileType, withQuotes))
}
if (StringLiteralTrim.matches(contents, withQuotes)) {
add(StringLiteralTrim(location, contents, fileType, withQuotes))
}
}
class NumberLiteral(
location: Location,
original: String,
fileType: Source.FileType,
private val isNegative: Boolean = false,
private val isDivision: Boolean = false,
private val base: Int = 10,
) : Mutation(Type.NUMBER_LITERAL, location, original, fileType) {
override val preservesLength = true
override val mightNotCompile = false
override val fixedCount = false
private val numberPositions = original.toCharArray()
.mapIndexed { index, c -> Pair(index, c) }
.filter { (index, c) -> c.isDigit() && (base != 8 || index > 0) }
.map { it.first }.also {
check(it.isNotEmpty()) { "No numeric characters in numeric literal" }
}
override val estimatedCount = if (original == "0") {
1
} else if (isDivision && original == "1") {
1
} else if (isNegative && (original == "1" || original == (base - 1).toString())) {
1
} else {
numberPositions.size * 2
}
override fun applyMutation(random: Random): String {
// Special case since 0 -> 9 is a bit too obvious
if (original == "0") {
return "1"
}
if (original == "1" && isDivision) {
return "2"
}
val position = numberPositions.shuffled(random).first()
return original.toCharArray().also { characters ->
var direction = random.nextBoolean()
if (original == "1" && isNegative) {
direction = true
} else if (original == (base - 1).toString() && isNegative) {
direction = false
}
val randomValue = if (direction) {
Math.floorMod(characters[position].toString().toInt() + 1, base)
} else {
Math.floorMod(characters[position].toString().toInt() - 1 + base, base)
}.let {
// Avoid adding leading zeros
if (position == 0 && original.length > 1 && it == 0) {
if (direction) {
1
} else {
9
}
} else {
it
}
}
// Sadder than it needs to be, since int <-> char conversions in Kotlin use ASCII values
characters[position] = randomValue.toString().toCharArray()[0]
}.let { String(it) }
}
}
class NumberLiteralTrim(
location: Location,
original: String,
fileType: Source.FileType,
base: Int,
isNegative: Boolean = false,
isDivision: Boolean = false,
) : Mutation(Type.NUMBER_LITERAL_TRIM, location, original, fileType) {
override val preservesLength = false
override val mightNotCompile = false
override val fixedCount = true
private val options = original.trims(base, isNegative, isDivision)
override val estimatedCount = options.size
override fun applyMutation(random: Random) = options.shuffled(random).first()
companion object {
private val hexSet = setOf('a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F')
private val exponentRegex = Regex("[eE][+-]?[0-9]+$")
private val suffixes = setOf('f', 'F', 'd', 'D', 'l', 'L')
private fun String.trims(passedBase: Int, isNegative: Boolean, isDivision: Boolean): List {
val base = if (length >= 2 && startsWith("0") && this[1].isDigit()) {
8
} else {
passedBase
}
var current = this
val prefix = when (base) {
10 -> ""
2 -> "0b"
16 -> "0x"
8 -> "0"
else -> error("Invalid base $base")
}
check(current.startsWith(prefix))
current = current.removePrefix(prefix)
check(current.isNotEmpty())
val suffix = if (suffixes.contains(current.last())) {
current.last().toString()
} else {
""
}.let {
if (base == 16 && it.uppercase() == "F") {
""
} else {
it
}
}
check(current.endsWith(suffix))
current = current.removeSuffix(suffix)
val exponent = exponentRegex.find(current)?.value ?: ""
check(current.endsWith(exponent))
current = current.removeSuffix(exponent)
check(current.isNotEmpty())
return when {
current.length == 1 -> listOf()
current.contains(".") -> {
val parts = current.split(".")
check(parts.size == 2)
val (front, back) = parts
val frontTrimmed = if (front.isNotEmpty()) front.substring(1) else front
val backTrimmed = if (back.isNotEmpty()) back.substring(0, back.length - 1) else back
listOf("$front.$backTrimmed", "$frontTrimmed.$back").filter {
!it.endsWith(".")
}.filter {
it != current && it != "."
}.filter {
!(front == "0" && it.startsWith(".")) &&
!(back == "0" && it.endsWith(".")) &&
!(back.endsWith("0") && it.endsWith("0"))
}
}
else -> listOf(current.substring(1, current.length), current.substring(0, current.length - 1))
}
.filter { string ->
string.length == 1 ||
!string.toCharArray().filter {
if (base == 16) {
it.isDigit() || hexSet.contains(it)
} else {
it.isDigit()
}
}.all { it == '0' }
}
.filter { !((isNegative || isDivision) && it == "0") }
.map { "$prefix$it$exponent$suffix" }
}
fun matches(contents: String, base: Int, isNegative: Boolean, isDivision: Boolean) =
contents.trims(base, isNegative = isNegative, isDivision = isDivision).isNotEmpty()
}
}
class IncrementDecrement(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.INCREMENT_DECREMENT, location, original, fileType) {
override val preservesLength = true
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
INC -> DEC
DEC -> INC
else -> error("${javaClass.name} didn't find the expected text")
}
companion object {
fun matches(contents: String) = contents in setOf(INC, DEC)
private const val INC = "++"
private const val DEC = "--"
}
}
class InvertNegation(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.INVERT_NEGATION, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
companion object {
fun matches(contents: String) = contents == "-"
}
}
class MutateMath(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.MATH, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
SUBTRACT -> ADD
MULTIPLY -> DIVIDE
DIVIDE -> MULTIPLY
REMAINDER -> MULTIPLY
BITWISE_AND -> BITWISE_OR
BITWISE_OR -> BITWISE_AND
BITWISE_XOR -> BITWISE_AND
LEFT_SHIFT -> RIGHT_SHIFT
RIGHT_SHIFT -> LEFT_SHIFT
UNSIGNED_RIGHT_SHIFT -> LEFT_SHIFT
else -> error("${javaClass.name} didn't find the expected text")
}
companion object {
const val ADD = "+"
const val SUBTRACT = "-"
const val MULTIPLY = "*"
const val DIVIDE = "/"
const val REMAINDER = "%"
const val BITWISE_AND = "&"
const val BITWISE_OR = "|"
const val BITWISE_XOR = "^"
const val LEFT_SHIFT = "<<"
const val RIGHT_SHIFT = ">>"
const val UNSIGNED_RIGHT_SHIFT = ">>>"
fun matches(contents: String) = contents in setOf(
SUBTRACT, MULTIPLY, DIVIDE, REMAINDER,
BITWISE_AND, BITWISE_OR, BITWISE_XOR,
LEFT_SHIFT, RIGHT_SHIFT, UNSIGNED_RIGHT_SHIFT,
)
}
}
class PlusToMinus(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.PLUS_TO_MINUS, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = true
override val fixedCount = true
override fun applyMutation(random: Random) = "-"
companion object {
fun matches(contents: String) = contents == "+"
}
}
object Conditionals {
const val EQ = "=="
const val NE = "!="
const val LT = "<"
const val LTE = "<="
const val GT = ">"
const val GTE = ">="
}
class ConditionalBoundary(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.CONDITIONAL_BOUNDARY, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
Conditionals.LT -> Conditionals.LTE
Conditionals.LTE -> Conditionals.LT
Conditionals.GT -> Conditionals.GTE
Conditionals.GTE -> Conditionals.GT
else -> error("${javaClass.name} didn't find the expected text")
}
companion object {
fun matches(contents: String) = contents in setOf(
Conditionals.LT,
Conditionals.LTE,
Conditionals.GT,
Conditionals.GTE,
)
}
}
class NegateConditional(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.NEGATE_CONDITIONAL, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
Conditionals.EQ -> Conditionals.NE
Conditionals.NE -> Conditionals.EQ
Conditionals.LTE -> Conditionals.GT
Conditionals.GT -> Conditionals.LTE
Conditionals.GTE -> Conditionals.LT
Conditionals.LT -> Conditionals.GTE
else -> error("${javaClass.name} didn't find the expected text")
}
companion object {
fun matches(contents: String) = contents in setOf(
Conditionals.EQ,
Conditionals.NE,
Conditionals.LT,
Conditionals.LTE,
Conditionals.GT,
Conditionals.GTE,
)
}
}
class SwapAndOr(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.SWAP_AND_OR, location, original, fileType) {
override val preservesLength = true
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
"&&" -> "||"
"||" -> "&&"
else -> error("${javaClass.name} didn't find the expected text")
}
companion object {
fun matches(contents: String) = contents in setOf("&&", "||")
}
}
class SwapBreakContinue(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.SWAP_BREAK_CONTINUE, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
"break" -> "continue"
"continue" -> "break"
else -> error("${javaClass.name} didn't find the expected text")
}
}
class PlusOrMinusOneToZero(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.PLUS_OR_MINUS_ONE_TO_ZERO, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = when (original) {
"1" -> "0"
else -> error("${javaClass.name} didn't find the expected text: $original")
}
}
private val javaPrimitiveTypes = setOf("byte", "short", "int", "long", "float", "double", "char", "boolean")
private val kotlinPrimitiveTypes = setOf("Byte", "Short", "Int", "Long", "Float", "Double", "Char", "Boolean")
class PrimitiveReturn(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.PRIMITIVE_RETURN, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = "0"
companion object {
private val zeros = setOf("0", "0L", "0.0", "0.0f")
fun matches(contents: String, returnType: String, fileType: Source.FileType) = when (fileType) {
Source.FileType.JAVA -> contents !in zeros && returnType in (javaPrimitiveTypes - "boolean")
Source.FileType.KOTLIN -> contents !in zeros && returnType in (kotlinPrimitiveTypes - "Boolean")
}
}
}
class TrueReturn(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.TRUE_RETURN, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = "true"
companion object {
fun matches(contents: String, returnType: String) =
contents != "true" && returnType in setOf("boolean", "Boolean")
}
}
class FalseReturn(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.FALSE_RETURN, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = "false"
companion object {
fun matches(contents: String, returnType: String) =
contents != "false" && returnType in setOf("boolean", "Boolean")
}
}
class NullReturn(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.NULL_RETURN, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = "null"
companion object {
@Suppress("DEPRECATION")
fun matches(contents: String, returnType: String, fileType: Source.FileType) = when (fileType) {
Source.FileType.JAVA ->
contents != "null" &&
(
returnType == returnType.capitalize() ||
returnType.endsWith("[]")
)
Source.FileType.KOTLIN -> contents != "null" && !kotlinPrimitiveTypes.contains(returnType)
}
}
}
class RemoveRuntimeCheck(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_RUNTIME_CHECK, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemoveMethod(
location: Location,
original: String,
private val returnType: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_METHOD, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
private val prefix = getPrefix(original)
private val postfix = getPostfix(original)
override fun applyMutation(random: Random) = forReturnType(returnType, fileType).let {
when (fileType) {
Source.FileType.JAVA -> "$prefix$it;$postfix"
Source.FileType.KOTLIN -> "$prefix$it$postfix"
}
}
companion object {
@Suppress("ComplexMethod")
private fun forReturnType(returnType: String, fileType: Source.FileType) = when (fileType) {
Source.FileType.JAVA -> when (returnType) {
"String" -> "return \"\""
"void" -> "return"
"byte" -> "return 0"
"short" -> "return 0"
"int" -> "return 0"
"long" -> "return 0L"
"char" -> "return '0'"
"boolean" -> "return false"
"float" -> "return 0.0f"
"double" -> "return 0.0"
else -> "return null"
}
Source.FileType.KOTLIN -> when (returnType.removeSuffix("?")) {
"String" -> "return \"\""
"" -> "return"
"Byte" -> "return 0"
"Short" -> "return 0"
"Int" -> "return 0"
"Long" -> "return 0L"
"Char" -> "return '0'"
"Boolean" -> "return false"
"Float" -> "return 0.0f"
"Double" -> "return 0.0"
else -> "return null"
}
}
private fun getPrefix(content: String): String {
var prefix = ""
var seenBrace = false
for (char in content.toCharArray()) {
if (seenBrace && !char.isWhitespace()) {
break
}
if (char == '{') {
seenBrace = true
}
prefix += char
}
return prefix
}
private fun getPostfix(content: String): String {
var postfix = ""
var seenBrace = false
for (char in content.toCharArray().reversed()) {
if (seenBrace && !char.isWhitespace()) {
break
}
if (char == '}') {
seenBrace = true
}
postfix += char
}
return postfix.reversed()
}
fun matches(contents: String, returnType: String, fileType: Source.FileType) =
contents.removePrefix(getPrefix(contents)).let {
it.removeSuffix(getPostfix(it))
}.let {
it.isNotBlank() && it.trim().removeSuffix(";").trim() != forReturnType(returnType, fileType)
}
}
}
class NegateIf(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.NEGATE_IF, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random) = when (fileType) {
Source.FileType.JAVA -> "(!$original)"
Source.FileType.KOTLIN -> "!($original)"
}
}
class NegateWhile(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.NEGATE_WHILE, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random) = when (fileType) {
Source.FileType.JAVA -> "(!$original)"
Source.FileType.KOTLIN -> "!($original)"
}
}
class RemoveIf(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_IF, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = true
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemoveLoop(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_LOOP, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemoveAndOr(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_AND_OR, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemoveTry(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_TRY, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemoveStatement(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_STATEMENT, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = when (fileType) {
Source.FileType.JAVA -> false
Source.FileType.KOTLIN -> true
}
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
}
class RemovePlus(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_PLUS, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = true
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
companion object {
fun matches(contents: String) = contents == "+"
}
}
class RemoveBinary(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.REMOVE_BINARY, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = true
override fun applyMutation(random: Random): String = ""
companion object {
private val BINARY = setOf("-", "*", "/", "%", "&", "|", "^", "<<", ">>", ">>>")
fun matches(contents: String) = BINARY.contains(contents)
}
}
class ChangeEquals(
location: Location,
original: String,
fileType: Source.FileType,
private val originalEqualsType: String = "",
private val firstValue: String = "",
private val secondValue: String = "",
) : Mutation(Type.CHANGE_EQUALS, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = true
override val fixedCount = true
override fun applyMutation(random: Random): String {
when (fileType) {
Source.FileType.KOTLIN -> {
return when (originalEqualsType) {
"==" -> "==="
"===" -> "=="
else -> error("Unknown kotlin equals type: $originalEqualsType")
}
}
Source.FileType.JAVA -> {
return when (originalEqualsType) {
"==" -> "($firstValue).equals($secondValue)"
".equals" -> "($firstValue) == ($secondValue)"
else -> error("Unknown java equals type: $originalEqualsType")
}
}
}
}
}
class AddBreak(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.ADD_BREAK, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = false
override fun applyMutation(random: Random): String = when (fileType) {
Source.FileType.JAVA -> "break; }"
Source.FileType.KOTLIN -> "break }"
}
}
class AddContinue(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.ADD_CONTINUE, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 1
override val mightNotCompile = false
override val fixedCount = false
override fun applyMutation(random: Random): String = when (fileType) {
Source.FileType.JAVA -> "continue; }"
Source.FileType.KOTLIN -> "continue }"
}
}
class ModifyArrayLiteral(
location: Location,
original: String,
fileType: Source.FileType,
private val parts: List,
) : Mutation(Type.MODIFY_ARRAY_LITERAL, location, original, fileType) {
init {
check(parts.size > 1)
}
override val preservesLength = false
override val estimatedCount = parts.size - 1
override val mightNotCompile = false
override val fixedCount = false
override fun applyMutation(random: Random): String {
val toRemove = parts.indices.shuffled(random).first()
val separator = when (fileType) {
Source.FileType.JAVA -> ","
Source.FileType.KOTLIN -> ", "
}
return parts.filterIndexed { i, _ ->
i != toRemove
}.joinToString(separator).trim()
}
}
class ChangeLengthAndSize(
location: Location,
original: String,
fileType: Source.FileType,
) : Mutation(Type.MODIFY_LENGTH_AND_SIZE, location, original, fileType) {
override val preservesLength = false
override val estimatedCount = 2
override val mightNotCompile = true
override val fixedCount = true
init {
when (fileType) {
Source.FileType.JAVA -> check(original in javaLengthAndSize) { "Invalid length or size: $original" }
Source.FileType.KOTLIN -> check(original in kotlinLengthAndSize) { "Invalid length or size: $original" }
}
}
override fun applyMutation(random: Random): String = random.nextBoolean().let {
if (it) {
"$original + 1"
} else {
"$original - 1"
}
}
companion object {
val javaLengthAndSize = listOf("length", "length()", "size()")
val kotlinLengthAndSize = listOf("length", "size")
}
}