d.core.2024.8.0.source-code.Mutater.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.
@file:Suppress("MatchingDeclarationName", "TooManyFunctions")
package edu.illinois.cs.cs125.jeed.core
import com.github.difflib.DiffUtils
import com.github.difflib.patch.DeltaType
import com.squareup.moshi.JsonClass
import org.antlr.v4.runtime.misc.Interval
import kotlin.random.Random
fun Source.ParsedSource.contents(location: Mutation.Location): String =
stream.getText(Interval(location.start, location.end))
fun MutableList.klass(): String =
findLast { it.type == Mutation.Location.SourcePath.Type.CLASS }?.name ?: error("No current class in path")
fun MutableList.method(): String =
findLast { it.type == Mutation.Location.SourcePath.Type.METHOD }?.name ?: error("No current method in path")
@JsonClass(generateAdapter = true)
data class SourceMutation(
val name: String,
val mutation: Mutation,
)
@JsonClass(generateAdapter = true)
data class AppliedSourceMutation(
val name: String,
val mutation: AppliedMutation,
) {
constructor(sourceMutation: SourceMutation) : this(sourceMutation.name, AppliedMutation(sourceMutation.mutation))
}
@Suppress("unused", "MemberVisibilityCanBePrivate")
class MutatedSource(
sources: Sources,
val originalSources: Sources,
val mutations: List,
val appliedMutations: Int,
@Suppress("SpellCheckingInspection") val unappliedMutations: Int,
) : Source(sources) {
fun cleaned() = Source(sources.mapValues { removeMutationSuppressions(it.value) })
suspend fun formatted(): MutatedSource {
val formattedSources = when (type) {
SourceType.JAVA -> googleFormat()
SourceType.KOTLIN -> ktFormat()
else -> error("Can't mutate mixed sources")
}
return MutatedSource(formattedSources.sources, originalSources, mutations, appliedMutations, unappliedMutations)
}
@Suppress("LongMethod")
fun oldMarked(): Source {
require(mutations.size == 1) { "Can only mark sources that have been mutated once" }
val mutation = mutations.first()
return Source(
sources.mapValues { (name, modified) ->
if (name != mutation.name) {
return@mapValues modified
}
val original = originalSources[name] ?: error("Didn't find original sources")
check(original != modified) { "Didn't find mutation" }
val originalLines = original.lines()
val modifiedLines = modified.lines()
val deltas =
DiffUtils.diff(originalLines, modifiedLines).deltas.sortedBy { it.source.position }.toMutableList()
var i = 0
val output = mutableListOf()
while (i < originalLines.size) {
val line = originalLines[i]
val indentAmount = line.length - line.trimStart().length
val currentIndent = " ".repeat(indentAmount)
val activeDelta = deltas.firstOrNull()?.let {
if (it.source.position == i) {
it
} else {
null
}
}
val sourceLines = activeDelta?.source?.lines
val targetLines = activeDelta?.target?.lines
val nextLine = when {
activeDelta == null -> {
i++
line
}
activeDelta.type == DeltaType.CHANGE -> {
val originalContent = sourceLines!!.joinToString("\n") {
if (it.length < indentAmount) {
"$currentIndent//"
} else {
currentIndent + "// " + it.substring(indentAmount)
}
}
i += sourceLines.size
deltas.removeAt(0)
"""
|$currentIndent// Modified by ${mutation.mutation.mutationType.mutationName()}. Originally:
|$originalContent
|${targetLines!!.joinToString("\n")}
""".trimMargin()
}
activeDelta.type == DeltaType.DELETE -> {
val sourceIndent = sourceLines!!.minOfOrNull {
it.length - it.trimStart().length
}!!
val originalContent = sourceLines.joinToString("\n") {
assert(it.length >= sourceIndent)
currentIndent + "// " + it.substring(sourceIndent).trimEnd()
}
i += sourceLines.size
"""
|$currentIndent// Removed by ${mutation.mutation.mutationType.mutationName()}. Originally:
|$originalContent
""".trimMargin()
}
else -> error("Invalid delta type: ${activeDelta.type}")
}
output += nextLine
}
output.joinToString("\n")
},
)
}
fun marked(): Source {
require(mutations.size == 1) { "Can only mark sources that have been mutated once" }
val mutation = mutations.first()
return Source(
sources.mapValues { (name, modified) ->
if (name != mutation.name) {
return@mapValues modified
}
val original = originalSources[name] ?: error("Didn't find original sources")
check(original != modified) { "Didn't find mutation" }
val originalLines = original.lines()
val modifiedLines = modified.lines()
val deltas =
DiffUtils.diff(originalLines, modifiedLines).deltas.sortedBy { it.source.position }.toMutableList()
check(deltas.size >= 1) { "Didn't find a delta" }
var currentOriginalLine = 0
val output = mutableListOf()
for (delta in deltas) {
check(currentOriginalLine <= delta.source.position)
while (currentOriginalLine < delta.source.position) {
output.add(originalLines[currentOriginalLine])
currentOriginalLine++
}
val nextIndentAmount = delta.source.lines?.firstOrNull()?.let {
it.length - it.trimStart().length
} ?: delta.target.lines?.firstOrNull()!!.let {
it.length - it.trimStart().length
}
val nextIndent = " ".repeat(nextIndentAmount)
val deltaAmount = delta.source.lines?.minOfOrNull {
it.length - it.trimStart().length
} ?: delta.target.lines?.minOfOrNull {
it.length - it.trimStart().length
}!!
val originalContent = delta.source.lines!!.joinToString("\n") {
assert(it.length >= deltaAmount)
nextIndent + "// " + it.substring(deltaAmount).trimEnd()
}
when (delta.type) {
DeltaType.CHANGE -> {
output.add(
"""
|$nextIndent// Modified by ${mutation.mutation.mutationType.mutationName()}. Originally:
|$originalContent
""".trimMargin(),
)
output.addAll(delta.target.lines)
}
DeltaType.INSERT -> {
output.add(
"""
|$nextIndent// Inserted by ${mutation.mutation.mutationType.mutationName()}.
""".trimMargin(),
)
output.addAll(delta.target.lines)
}
DeltaType.DELETE -> {
output.add(
"""|$nextIndent// Removed by ${mutation.mutation.mutationType.mutationName()}. Originally:
|$originalContent
""".trimMargin(),
)
}
else -> error("Bad delta type ${delta.type}")
}
if (delta.type == DeltaType.CHANGE || delta.type == DeltaType.DELETE) {
currentOriginalLine += delta.source.lines.size
} else {
check(delta.type == DeltaType.INSERT)
}
}
for (i in currentOriginalLine until originalLines.size) {
output.add(originalLines[i])
}
output.joinToString("\n")
},
)
}
companion object {
private val matchMutationSuppression = Regex("""\s*// mutate-disable.*$""")
fun removeMutationSuppressions(contents: String) = contents.lines().filter {
!it.trim().startsWith("""// mutate-disable""")
}.joinToString("\n") {
matchMutationSuppression.replace(it, "")
}
}
}
class Mutater(
private val originalSource: Source,
shuffle: Boolean,
seed: Int,
types: Set,
) {
private val random = Random(seed)
private val mutations = originalSource.sources.keys.map { name ->
Mutation.find(originalSource.getParsed(name), originalSource.type.toFileType())
.map { mutation -> SourceMutation(name, mutation) }
.filter {
types.contains(it.mutation.mutationType)
}
}.flatten().let {
if (shuffle) {
it.shuffled(random)
} else {
it
}
}
private val availableMutations: MutableList = mutations.toMutableList()
val appliedMutations: MutableList = mutableListOf()
val size: Int
get() = availableMutations.size
val sources = originalSource.sources.toMutableMap()
internal fun apply(): Sources {
check(availableMutations.isNotEmpty()) { "No more mutations to apply" }
availableMutations.removeAt(0).also { sourceMutation ->
val original = sources[sourceMutation.name] ?: error("Couldn't find key that should be there")
val modified = sourceMutation.mutation.apply(original, random)
check(original != modified) { "Mutation did not change source" }
appliedMutations.add(AppliedSourceMutation(sourceMutation))
availableMutations.removeIf { it.mutation.overlaps(sourceMutation.mutation) }
availableMutations.filter { it.mutation.after(sourceMutation.mutation) }.forEach {
it.mutation.shift(modified.length - original.length)
}
sources[sourceMutation.name] = modified
}
return Sources(sources)
}
fun mutate(limit: Int = 1): MutatedSource {
check(appliedMutations.isEmpty()) { "Some mutations already applied" }
@Suppress("UnusedPrivateMember")
for (unused in 0 until limit) {
if (availableMutations.isEmpty()) {
break
}
apply()
}
return MutatedSource(
Sources(sources),
originalSource.sources,
appliedMutations,
appliedMutations.size,
availableMutations.size,
)
}
}
fun Source.mutater(shuffle: Boolean = true, seed: Int = Random.nextInt(), types: Set = ALL) =
Mutater(this, shuffle, seed, types = types)
fun Source.mutate(
shuffle: Boolean = true,
seed: Int = Random.nextInt(),
limit: Int = 1,
types: Set = Mutation.Type.entries.toSet(),
) =
Mutater(this, shuffle, seed, types).mutate(limit)
fun SourceMutation.suppressed(contents: String) = mutation.location.line.lines().any { line ->
line.split("""//""").let { parts ->
parts.size == 2 &&
(
parts[1].split(" ").contains("mutate-disable") ||
parts[1].split(" ").contains(mutation.mutationType.suppressionComment())
)
}
} ||
contents.lines().let { lines ->
for (lineNumber in mutation.location.startLine - 1 downTo 1) {
val line = lines[lineNumber - 1].trim()
if (!line.startsWith("""//""")) {
break
}
line.split("""//""").also { parts ->
if (parts.size == 2 &&
(
parts[1].split(" ").contains("mutate-disable") ||
parts[1].split(" ").contains(mutation.mutationType.suppressionComment())
)
) {
return@let true
}
}
}
false
}
fun Source.allMutations(
suppressWithComments: Boolean = true,
random: Random = Random,
types: Set = ALL,
): List {
val mutations = sources.keys.map { name ->
Mutation.find(getParsed(name), type.toFileType()).map { mutation -> SourceMutation(name, mutation) }
}.flatten()
.filter { types.contains(it.mutation.mutationType) }
.filter { !suppressWithComments || !it.suppressed(sources[it.name]!!) }
return mutations.map { sourceMutation ->
val modifiedSources = sources.copy().toMutableMap()
val original =
modifiedSources[sourceMutation.name] ?: error("Couldn't find a source that should be there")
val modified = sourceMutation.mutation.apply(original, random)
check(original != modified) { "Mutation did not change source" }
modifiedSources[sourceMutation.name] = modified
MutatedSource(
Sources(modifiedSources),
sources,
listOf(AppliedSourceMutation(sourceMutation)),
1,
mutations.size - 1,
)
}
}
fun Source.mutationStream(
suppressWithComments: Boolean = true,
random: Random = Random,
types: Set = ALL,
retryCount: Int = 32,
) = sequence {
val mutations = sources.keys.asSequence().map { name ->
Mutation.find(getParsed(name), type.toFileType()).map { mutation -> SourceMutation(name, mutation) }
}.flatten()
.filter { types.contains(it.mutation.mutationType) }
.filter { !suppressWithComments || !it.suppressed(sources[it.name]!!) }
.toMutableList()
val seen = mutableSetOf()
var retries = 0
val remaining = mutableMapOf()
@Suppress("LoopWithTooManyJumpStatements")
while (true) {
val mutation = mutations.shuffled(random).first()
if (mutation !in remaining) {
remaining[mutation] = mutation.mutation.estimatedCount
}
mutation.mutation.reset()
val modifiedSources = sources.copy().toMutableMap()
val original = modifiedSources[mutation.name] ?: error("Couldn't find a source that should be there")
val modified = mutation.mutation.apply(original, random)
check(original != modified) { "Mutation did not change source" }
modifiedSources[mutation.name] = modified
val source = MutatedSource(
Sources(modifiedSources),
sources,
listOf(AppliedSourceMutation(mutation)),
1,
mutations.size - 1,
)
if (source.md5 !in seen) {
retries = 0
seen += source.md5
yield(source)
remaining[mutation] = remaining[mutation]!! - 1
if (remaining[mutation] == 0) {
mutations.remove(mutation)
if (mutations.isEmpty()) {
return@sequence
}
}
} else {
if (retries++ >= retryCount) {
return@sequence
}
}
}
}
@Suppress("NestedBlockDepth", "LongParameterList")
fun Source.allFixedMutations(
suppressWithComments: Boolean = true,
random: Random = Random,
types: Set = ALL,
nonFixedMax: Int = 4,
retryCount: Int = 8,
): List {
val mutations = sources.keys.asSequence()
.map {
Mutation.find(getParsed(it), type.toFileType()).map { mutation -> SourceMutation(name, mutation) }
}.flatten()
.filter { types.contains(it.mutation.mutationType) }
.filter { !suppressWithComments || !it.suppressed(sources[it.name]!!) }
.toMutableList()
val mutatedSources = mutableListOf()
for (mutation in mutations) {
val seen = mutableSetOf()
var retries = 0
val count = if (mutation.mutation.fixedCount) {
mutation.mutation.estimatedCount
} else {
mutation.mutation.estimatedCount.coerceAtMost(nonFixedMax)
}
@Suppress("UnusedPrivateMember")
for (unused in 0 until count) {
mutation.mutation.reset()
val modifiedSources = sources.copy().toMutableMap()
val original = modifiedSources[mutation.name] ?: error("Couldn't find a source that should be there")
val modified = mutation.mutation.apply(original, random)
check(original != modified) { "Mutation did not change source" }
modifiedSources[mutation.name] = modified
val source = MutatedSource(
Sources(modifiedSources),
sources,
listOf(AppliedSourceMutation(mutation)),
1,
mutations.size - 1,
)
if (source.md5 !in seen) {
retries = 0
seen += source.md5
mutatedSources += source
} else {
if (retries++ >= retryCount) {
break
}
}
}
}
return mutatedSources
}
@JsonClass(generateAdapter = true)
data class MutationsArguments(val limit: Int = 4, val suppressWithComments: Boolean = true)
class MutationsFailed(errors: List) : JeedError(errors) {
override fun toString(): String = "errors were encountered while mutating sources: ${errors.joinToString(separator = ",")}"
}
@JsonClass(generateAdapter = true)
data class MutationsResults(val source: Map, val mutatedSources: List) {
@JsonClass(generateAdapter = true)
data class MutatedSource(
val mutatedSource: String,
val mutatedSources: Map,
val mutation: AppliedMutation,
)
}
@Throws(MutationsFailed::class)
fun Source.mutations(mutationsArguments: MutationsArguments = MutationsArguments()): MutationsResults {
try {
val mutatedSources = mutationStream(mutationsArguments.suppressWithComments)
.map {
require(it.mutations.size == 1) { "Stream applied multiple mutations" }
MutationsResults.MutatedSource(
it.mutations.first().name,
it.sources.sources,
it.mutations.first().mutation,
)
}
.take(mutationsArguments.limit)
.toList()
return MutationsResults(this.sources, mutatedSources)
} catch (e: JeedParsingException) {
throw MutationsFailed(e.errors)
}
}
fun String.trimParentheses(): String {
var current = this
while (true) {
val next = current.removePrefix("(").removeSuffix(")")
if (current == next) {
return next
}
current = next
}
}